feat(comment): 实现评论热度排序及缓存优化
- 修改 CommentDO 中 heat 字段类型从 BigDecimal为 Double - 新增 selectHeatComments 方法用于查询热门评论- 优化评论分页查询逻辑,引入 Redis 缓存提升性能 - 新增评论总数与热门评论的 Redis 缓存同步机制 - 实现评论详情的 Redis 批量缓存与过期策略 - 添加 COMMENT_NOT_FOUND 业务异常码 - 更新 RedisKeyConstants 增加相关键构建方法 - 调整 XML 映射文件以支持新的查询与字段类型- 引入 RedisTemplate 和线程池异步处理缓存操作 - 在 FindCommentItemRspVO 中新增 heat 字段返回热度值
This commit is contained in:
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@@ -6,6 +6,7 @@
|
||||
<w>hannote</w>
|
||||
<w>hanserwei</w>
|
||||
<w>jobhandler</w>
|
||||
<w>mget</w>
|
||||
<w>nacos</w>
|
||||
<w>operationlog</w>
|
||||
<w>rustfs</w>
|
||||
|
||||
@@ -7,6 +7,26 @@ public class RedisKeyConstants {
|
||||
*/
|
||||
private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:";
|
||||
|
||||
/**
|
||||
* Hash Field 键:评论总数
|
||||
*/
|
||||
public static final String FIELD_COMMENT_TOTAL = "commentTotal";
|
||||
|
||||
/**
|
||||
* Key 前缀:笔记评论总数
|
||||
*/
|
||||
private static final String COUNT_COMMENT_TOTAL_KEY_PREFIX = "count:note:";
|
||||
|
||||
/**
|
||||
* Key 前缀:评论分页 ZSET
|
||||
*/
|
||||
private static final String COMMENT_LIST_KEY_PREFIX = "comment:list:";
|
||||
|
||||
/**
|
||||
* Key 前缀:评论详情 JSON
|
||||
*/
|
||||
private static final String COMMENT_DETAIL_KEY_PREFIX = "comment:detail:";
|
||||
|
||||
|
||||
/**
|
||||
* 构建完整 KEY
|
||||
@@ -18,4 +38,34 @@ public class RedisKeyConstants {
|
||||
return HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX + commentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建笔记评论总数完整 KEY
|
||||
*
|
||||
* @param noteId 笔记 ID
|
||||
* @return 笔记评论总数完整 KEY
|
||||
*/
|
||||
public static String buildNoteCommentTotalKey(Long noteId) {
|
||||
return COUNT_COMMENT_TOTAL_KEY_PREFIX + noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建评论分页 ZSET 完整 KEY
|
||||
*
|
||||
* @param noteId 笔记 ID
|
||||
* @return 评论分页 ZSET 完整 KEY
|
||||
*/
|
||||
public static String buildCommentListKey(Long noteId) {
|
||||
return COMMENT_LIST_KEY_PREFIX + noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建评论详情完整 KEY
|
||||
*
|
||||
* @param commentId 评论 ID
|
||||
* @return 评论详情完整 KEY
|
||||
*/
|
||||
public static String buildCommentDetailKey(Object commentId) {
|
||||
return COMMENT_DETAIL_KEY_PREFIX + commentId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
@@ -121,7 +120,7 @@ public class CommentDO {
|
||||
* 评论热度
|
||||
*/
|
||||
@TableField(value = "heat")
|
||||
private BigDecimal heat;
|
||||
private Double heat;
|
||||
|
||||
/**
|
||||
* 最早回复的评论ID (只有一级评论需要)
|
||||
|
||||
@@ -74,4 +74,12 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
|
||||
* @return 二级评论
|
||||
*/
|
||||
List<CommentDO> selectTwoLevelCommentByIds(@Param("commentIds") List<Long> commentIds);
|
||||
|
||||
/**
|
||||
* 查询热门评论
|
||||
*
|
||||
* @param noteId 笔记 ID
|
||||
* @return 热门评论
|
||||
*/
|
||||
List<CommentDO> selectHeatComments(Long noteId);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
PARAM_NOT_VALID("COMMENT-10001", "参数错误"),
|
||||
|
||||
// ----------- 业务异常状态码 -----------
|
||||
COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -61,4 +61,9 @@ public class FindCommentItemRspVO {
|
||||
*/
|
||||
private FindCommentItemRspVO firstReplyComment;
|
||||
|
||||
/**
|
||||
* 热度值
|
||||
*/
|
||||
private Double heat;
|
||||
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
package com.hanserwei.hannote.comment.biz.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||
import com.hanserwei.framework.common.constant.DateConstants;
|
||||
import com.hanserwei.framework.common.exception.ApiException;
|
||||
import com.hanserwei.framework.common.response.PageResponse;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.framework.common.utils.DateUtils;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.comment.biz.constants.MQConstants;
|
||||
import com.hanserwei.hannote.comment.biz.constants.RedisKeyConstants;
|
||||
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.NoteCountDOMapper;
|
||||
import com.hanserwei.hannote.comment.biz.enums.ResponseCodeEnum;
|
||||
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.FindCommentPageListReqVO;
|
||||
@@ -29,12 +34,13 @@ import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -53,6 +59,10 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
private KeyValueRpcService keyValueRpcService;
|
||||
@Resource
|
||||
private UserRpcService userRpcService;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource(name = "taskExecutor")
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
@Override
|
||||
public Response<?> publishComment(PublishCommentReqVO publishCommentReqVO) {
|
||||
@@ -127,13 +137,33 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
// 每页展示一级评论数量
|
||||
int pageSize = 10;
|
||||
|
||||
// TODO: 先从缓存中查询
|
||||
// 构建评论总数 Redis Key
|
||||
String noteCommentTotalKey = RedisKeyConstants.buildNoteCommentTotalKey(noteId);
|
||||
// 先从 Redis 中查询该笔记的评论总数
|
||||
Number commentTotal = (Number) redisTemplate.opsForHash()
|
||||
.get(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL);
|
||||
long count = Objects.isNull(commentTotal) ? 0L : commentTotal.longValue();
|
||||
|
||||
// 查询评论总数
|
||||
Long count = noteCountDOMapper.selectCommentTotalByNoteId(noteId);
|
||||
// 若缓存不存在,则查询数据库
|
||||
if (Objects.isNull(commentTotal)) {
|
||||
// 查询评论总数 (从 t_note_count 笔记计数表查,提升查询性能, 避免 count(*))
|
||||
Long dbCount = noteCountDOMapper.selectCommentTotalByNoteId(noteId);
|
||||
|
||||
if (Objects.isNull(count)) {
|
||||
return PageResponse.success(null, pageNo, pageSize);
|
||||
// 若数据库中也不存在,则抛出业务异常
|
||||
if (Objects.isNull(dbCount)) {
|
||||
throw new ApiException(ResponseCodeEnum.COMMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
count = dbCount;
|
||||
// 异步将评论总数同步到 Redis 中
|
||||
threadPoolTaskExecutor.execute(() ->
|
||||
syncNoteCommentTotal2Redis(noteCommentTotalKey, dbCount)
|
||||
);
|
||||
}
|
||||
|
||||
// 若评论总数为 0,则直接响应
|
||||
if (count == 0) {
|
||||
return PageResponse.success(null, pageNo, 0);
|
||||
}
|
||||
|
||||
// 分页返回参数
|
||||
@@ -144,113 +174,267 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
commentRspVOS = Lists.newArrayList();
|
||||
// 计算分页查询的offset
|
||||
long offset = PageResponse.getOffset(pageNo, pageSize);
|
||||
//查询一级评论
|
||||
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
|
||||
// 过滤出所有最早回复的二级评论ID
|
||||
List<Long> twoLevelCommentIds = oneLevelCommentIds.stream()
|
||||
.map(CommentDO::getFirstReplyCommentId)
|
||||
.filter(e -> e != 0)
|
||||
.toList();
|
||||
// 查询二级评论
|
||||
Map<Long, CommentDO> commentIdAndDOMap = null;
|
||||
List<CommentDO> twoLevelCommentDOS = null;
|
||||
if (CollUtil.isNotEmpty(twoLevelCommentIds)) {
|
||||
twoLevelCommentDOS = commentDOMapper.selectTwoLevelCommentByIds(twoLevelCommentIds);
|
||||
// 转Map方便后续数据拼接
|
||||
commentIdAndDOMap = twoLevelCommentDOS.stream()
|
||||
.collect(Collectors.toMap(CommentDO::getId, e -> e));
|
||||
// 评论分页缓存使用 ZSET + STRING 实现
|
||||
// 构建评论 ZSET Key
|
||||
String commentZSetKey = RedisKeyConstants.buildCommentListKey(noteId);
|
||||
// 先判断 ZSET 是否存在
|
||||
boolean hasKey = redisTemplate.hasKey(commentZSetKey);
|
||||
|
||||
// 若不存在
|
||||
if (!hasKey) {
|
||||
// 异步将热点评论同步到 redis 中(最多同步 500 条)
|
||||
threadPoolTaskExecutor.execute(() ->
|
||||
syncHeatComments2Redis(commentZSetKey, noteId));
|
||||
}
|
||||
|
||||
// 调用KV服务需要的入参
|
||||
List<FindCommentContentReqDTO> findCommentContentReqDTOS = Lists.newArrayList();
|
||||
// 调用用户服务需要的入参
|
||||
List<Long> userIds = Lists.newArrayList();
|
||||
// 若 ZSET 缓存存在, 并且查询的是前 50 页的评论
|
||||
if (hasKey && offset < 500) {
|
||||
// 使用 ZRevRange 获取某篇笔记下,按热度降序排序的一级评论 ID
|
||||
Set<Object> commentIds = redisTemplate.opsForZSet()
|
||||
.reverseRangeByScore(commentZSetKey, -Double.MAX_VALUE, Double.MAX_VALUE, offset, pageSize);
|
||||
|
||||
// 一二级评论合并到一起
|
||||
List<CommentDO> allCommentDOS = Lists.newArrayList();
|
||||
CollUtil.addAll(allCommentDOS, oneLevelCommentIds);
|
||||
CollUtil.addAll(allCommentDOS, twoLevelCommentDOS);
|
||||
// 若结果不为空
|
||||
if (CollUtil.isNotEmpty(commentIds)) {
|
||||
// Set 转 List
|
||||
List<Object> commentIdList = Lists.newArrayList(commentIds);
|
||||
|
||||
// 循环提取RPC需要的入参数据
|
||||
allCommentDOS.forEach(commentDO -> {
|
||||
// 构建KV服务批量查询评论内容的入参
|
||||
boolean isContentEmpty = commentDO.getIsContentEmpty();
|
||||
if (!isContentEmpty) {
|
||||
FindCommentContentReqDTO findCommentContentReqDTO = FindCommentContentReqDTO.builder()
|
||||
.contentId(commentDO.getContentUuid())
|
||||
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(commentDO.getCreateTime()))
|
||||
.build();
|
||||
findCommentContentReqDTOS.add(findCommentContentReqDTO);
|
||||
}
|
||||
// 构建 MGET 批量查询评论详情的 Key 集合
|
||||
List<String> commentIdKeys = commentIdList.stream()
|
||||
.map(RedisKeyConstants::buildCommentDetailKey)
|
||||
.toList();
|
||||
|
||||
// 构建用户服务批量查询用户信息的入参
|
||||
userIds.add(commentDO.getUserId());
|
||||
});
|
||||
// MGET 批量获取评论数据
|
||||
List<Object> commentsJsonList = redisTemplate.opsForValue().multiGet(commentIdKeys);
|
||||
|
||||
// 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);
|
||||
// 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 commentDO : oneLevelCommentIds) {
|
||||
// 一级评论
|
||||
Long userId = commentDO.getUserId();
|
||||
FindCommentItemRspVO oneLevelCommentRspVO = FindCommentItemRspVO.builder()
|
||||
.userId(userId)
|
||||
.commentId(commentDO.getId())
|
||||
.imageUrl(commentDO.getImageUrl())
|
||||
.createTime(DateUtils.formatRelativeTime(commentDO.getCreateTime()))
|
||||
.likeTotal(commentDO.getLikeTotal())
|
||||
.childCommentTotal(commentDO.getChildCommentTotal())
|
||||
.build();
|
||||
// 用户信息
|
||||
if (userIdAndDTOMap != null) {
|
||||
setUserInfo(userIdAndDTOMap, userId, oneLevelCommentRspVO);
|
||||
}
|
||||
// 笔记内容
|
||||
setCommentContent(commentUuidAndContentMap, commentDO, oneLevelCommentRspVO);
|
||||
|
||||
// 二级评论
|
||||
Long firstReplyCommentId = commentDO.getFirstReplyCommentId();
|
||||
if (CollUtil.isNotEmpty(commentIdAndDOMap)) {
|
||||
CommentDO firstReplyCommentDO = commentIdAndDOMap.get(firstReplyCommentId);
|
||||
if (Objects.nonNull(firstReplyCommentDO)) {
|
||||
Long firstReplyCommentUserId = firstReplyCommentDO.getUserId();
|
||||
FindCommentItemRspVO firstReplyCommentRspVO = FindCommentItemRspVO.builder()
|
||||
.userId(firstReplyCommentDO.getUserId())
|
||||
.commentId(firstReplyCommentDO.getId())
|
||||
.imageUrl(firstReplyCommentDO.getImageUrl())
|
||||
.createTime(DateUtils.formatRelativeTime(firstReplyCommentDO.getCreateTime()))
|
||||
.likeTotal(firstReplyCommentDO.getLikeTotal())
|
||||
.build();
|
||||
if (userIdAndDTOMap != null) {
|
||||
setUserInfo(userIdAndDTOMap, firstReplyCommentUserId, firstReplyCommentRspVO);
|
||||
// 可能存在部分评论不在缓存中,已经过期被删除,这些评论 ID 需要提取出来,等会查数据库
|
||||
List<Long> expiredCommentIds = Lists.newArrayList();
|
||||
for (int i = 0; i < commentsJsonList.size(); i++) {
|
||||
String commentJson = (String) commentsJsonList.get(i);
|
||||
if (Objects.nonNull(commentJson)) {
|
||||
// 缓存中存在的评论 Json,直接转换为 VO 添加到返参集合中
|
||||
FindCommentItemRspVO commentRspVO = JsonUtils.parseObject(commentJson, FindCommentItemRspVO.class);
|
||||
commentRspVOS.add(commentRspVO);
|
||||
} else {
|
||||
// 评论失效,添加到失效评论列表
|
||||
expiredCommentIds.add(Long.valueOf(commentIdList.get(i).toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
oneLevelCommentRspVO.setFirstReplyComment(firstReplyCommentRspVO);
|
||||
// 笔记内容
|
||||
setCommentContent(commentUuidAndContentMap, firstReplyCommentDO, firstReplyCommentRspVO);
|
||||
// 对于不存在的一级评论,需要批量从数据库中查询,并添加到 commentRspVOS 中
|
||||
if (CollUtil.isNotEmpty(expiredCommentIds)) {
|
||||
List<CommentDO> commentDOS = commentDOMapper.selectByCommentIds(expiredCommentIds);
|
||||
getCommentDataAndSync2Redis(commentDOS, noteId, commentRspVOS);
|
||||
}
|
||||
}
|
||||
commentRspVOS.add(oneLevelCommentRspVO);
|
||||
|
||||
// 按热度值进行降序排列
|
||||
commentRspVOS = commentRspVOS.stream()
|
||||
.sorted(Comparator.comparing(FindCommentItemRspVO::getHeat).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||
}
|
||||
// TODO 后续逻辑
|
||||
// 缓存中没有,则查询数据库
|
||||
//查询一级评论
|
||||
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
|
||||
getCommentDataAndSync2Redis(oneLevelCommentIds, noteId, commentRspVOS);
|
||||
}
|
||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一级评论数据,并同步到 Redis 中
|
||||
*
|
||||
* @param oneLevelCommentIds 一级评论ID列表
|
||||
* @param noteId 笔记ID
|
||||
* @param commentRspVOS 一级评论返回参数
|
||||
*/
|
||||
private void getCommentDataAndSync2Redis(List<CommentDO> oneLevelCommentIds, Long noteId, List<FindCommentItemRspVO> commentRspVOS) {
|
||||
// 过滤出所有最早回复的二级评论ID
|
||||
List<Long> twoLevelCommentIds = oneLevelCommentIds.stream()
|
||||
.map(CommentDO::getFirstReplyCommentId)
|
||||
.filter(e -> e != 0)
|
||||
.toList();
|
||||
// 查询二级评论
|
||||
Map<Long, CommentDO> commentIdAndDOMap = null;
|
||||
List<CommentDO> twoLevelCommentDOS = null;
|
||||
if (CollUtil.isNotEmpty(twoLevelCommentIds)) {
|
||||
twoLevelCommentDOS = commentDOMapper.selectTwoLevelCommentByIds(twoLevelCommentIds);
|
||||
// 转Map方便后续数据拼接
|
||||
commentIdAndDOMap = twoLevelCommentDOS.stream()
|
||||
.collect(Collectors.toMap(CommentDO::getId, e -> e));
|
||||
}
|
||||
|
||||
// 调用KV服务需要的入参
|
||||
List<FindCommentContentReqDTO> findCommentContentReqDTOS = Lists.newArrayList();
|
||||
// 调用用户服务需要的入参
|
||||
List<Long> userIds = Lists.newArrayList();
|
||||
|
||||
// 一二级评论合并到一起
|
||||
List<CommentDO> allCommentDOS = Lists.newArrayList();
|
||||
CollUtil.addAll(allCommentDOS, oneLevelCommentIds);
|
||||
CollUtil.addAll(allCommentDOS, twoLevelCommentDOS);
|
||||
|
||||
// 循环提取RPC需要的入参数据
|
||||
allCommentDOS.forEach(commentDO -> {
|
||||
// 构建KV服务批量查询评论内容的入参
|
||||
boolean isContentEmpty = commentDO.getIsContentEmpty();
|
||||
if (!isContentEmpty) {
|
||||
FindCommentContentReqDTO findCommentContentReqDTO = FindCommentContentReqDTO.builder()
|
||||
.contentId(commentDO.getContentUuid())
|
||||
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(commentDO.getCreateTime()))
|
||||
.build();
|
||||
findCommentContentReqDTOS.add(findCommentContentReqDTO);
|
||||
}
|
||||
|
||||
// 构建用户服务批量查询用户信息的入参
|
||||
userIds.add(commentDO.getUserId());
|
||||
});
|
||||
|
||||
// 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);
|
||||
// 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 commentDO : oneLevelCommentIds) {
|
||||
// 一级评论
|
||||
Long userId = commentDO.getUserId();
|
||||
FindCommentItemRspVO oneLevelCommentRspVO = FindCommentItemRspVO.builder()
|
||||
.userId(userId)
|
||||
.commentId(commentDO.getId())
|
||||
.imageUrl(commentDO.getImageUrl())
|
||||
.createTime(DateUtils.formatRelativeTime(commentDO.getCreateTime()))
|
||||
.likeTotal(commentDO.getLikeTotal())
|
||||
.childCommentTotal(commentDO.getChildCommentTotal())
|
||||
.heat(commentDO.getHeat())
|
||||
.build();
|
||||
// 用户信息
|
||||
if (userIdAndDTOMap != null) {
|
||||
setUserInfo(userIdAndDTOMap, userId, oneLevelCommentRspVO);
|
||||
}
|
||||
// 笔记内容
|
||||
setCommentContent(commentUuidAndContentMap, commentDO, oneLevelCommentRspVO);
|
||||
|
||||
// 二级评论
|
||||
Long firstReplyCommentId = commentDO.getFirstReplyCommentId();
|
||||
if (CollUtil.isNotEmpty(commentIdAndDOMap)) {
|
||||
CommentDO firstReplyCommentDO = commentIdAndDOMap.get(firstReplyCommentId);
|
||||
if (Objects.nonNull(firstReplyCommentDO)) {
|
||||
Long firstReplyCommentUserId = firstReplyCommentDO.getUserId();
|
||||
FindCommentItemRspVO firstReplyCommentRspVO = FindCommentItemRspVO.builder()
|
||||
.userId(firstReplyCommentDO.getUserId())
|
||||
.commentId(firstReplyCommentDO.getId())
|
||||
.imageUrl(firstReplyCommentDO.getImageUrl())
|
||||
.createTime(DateUtils.formatRelativeTime(firstReplyCommentDO.getCreateTime()))
|
||||
.likeTotal(firstReplyCommentDO.getLikeTotal())
|
||||
.heat(firstReplyCommentDO.getHeat())
|
||||
.build();
|
||||
if (userIdAndDTOMap != null) {
|
||||
setUserInfo(userIdAndDTOMap, firstReplyCommentUserId, firstReplyCommentRspVO);
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
oneLevelCommentRspVO.setFirstReplyComment(firstReplyCommentRspVO);
|
||||
// 笔记内容
|
||||
setCommentContent(commentUuidAndContentMap, firstReplyCommentDO, firstReplyCommentRspVO);
|
||||
}
|
||||
}
|
||||
commentRspVOS.add(oneLevelCommentRspVO);
|
||||
}
|
||||
// 异步将笔记详情,同步到 Redis 中
|
||||
threadPoolTaskExecutor.execute(() -> {
|
||||
// 准备批量写入的数据
|
||||
Map<String, String> data = Maps.newHashMap();
|
||||
commentRspVOS.forEach(commentRspVO -> {
|
||||
// 评论 ID
|
||||
Long commentId = commentRspVO.getCommentId();
|
||||
// 构建 Key
|
||||
String key = RedisKeyConstants.buildCommentDetailKey(commentId);
|
||||
data.put(key, JsonUtils.toJsonString(commentRspVO));
|
||||
});
|
||||
|
||||
// 使用 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 = RandomUtil.randomInt(5 * 60 * 60);
|
||||
|
||||
|
||||
// 批量写入并设置过期时间
|
||||
connection.setEx(
|
||||
redisTemplate.getStringSerializer().serialize(entry.getKey()),
|
||||
randomExpire,
|
||||
redisTemplate.getStringSerializer().serialize(jsonStr)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步笔记评论总数到 Redis 中
|
||||
*
|
||||
* @param noteCommentTotalKey 笔记评论总数 Redis key
|
||||
* @param dbCount 数据库查询到的笔记评论总数
|
||||
*/
|
||||
private void syncNoteCommentTotal2Redis(String noteCommentTotalKey, Long dbCount) {
|
||||
redisTemplate.executePipelined(new SessionCallback<>() {
|
||||
@Override
|
||||
public Object execute(RedisOperations operations) {
|
||||
// 同步 hash 数据
|
||||
operations.opsForHash().put(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL, dbCount);
|
||||
|
||||
// 随机过期时间 (保底1小时 + 随机时间),单位:秒
|
||||
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
||||
operations.expire(noteCommentTotalKey, expireTime, TimeUnit.SECONDS);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步热点评论至 Redis
|
||||
*
|
||||
* @param key 热门评论 Redis key
|
||||
* @param noteId 笔记 ID
|
||||
*/
|
||||
private void syncHeatComments2Redis(String key, Long noteId) {
|
||||
List<CommentDO> commentDOS = commentDOMapper.selectHeatComments(noteId);
|
||||
if (CollUtil.isNotEmpty(commentDOS)) {
|
||||
// 使用 Redis Pipeline 提升写入性能
|
||||
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
|
||||
|
||||
// 遍历评论数据并批量写入 ZSet
|
||||
for (CommentDO commentDO : commentDOS) {
|
||||
Long commentId = commentDO.getId();
|
||||
Double commentHeat = commentDO.getHeat();
|
||||
zSetOps.add(key, commentId, commentHeat);
|
||||
}
|
||||
|
||||
// 设置随机过期时间,单位:秒
|
||||
int randomExpiryTime = RandomUtil.randomInt(5 * 60 * 60); // 5小时以内
|
||||
redisTemplate.expire(key, randomExpiryTime, TimeUnit.SECONDS);
|
||||
return null; // 无返回值
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
|
||||
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
|
||||
<result column="child_comment_total" jdbcType="BIGINT" property="childCommentTotal"/>
|
||||
<result column="heat" jdbcType="DECIMAL" property="heat"/>
|
||||
<result column="heat" jdbcType="DOUBLE" property="heat"/>
|
||||
<result column="first_reply_comment_id" jdbcType="BIGINT" property="firstReplyCommentId"/>
|
||||
</resultMap>
|
||||
<sql id="Base_Column_List">
|
||||
@@ -45,21 +45,28 @@
|
||||
first_reply_comment_id
|
||||
</sql>
|
||||
|
||||
<select id="selectByCommentIds" parameterType="list" resultMap="BaseResultMap">
|
||||
<select id="selectByCommentIds" resultMap="BaseResultMap" parameterType="list">
|
||||
select id,
|
||||
user_id,
|
||||
content_uuid,
|
||||
is_content_empty,
|
||||
image_url,
|
||||
like_total,
|
||||
is_top,
|
||||
create_time,
|
||||
first_reply_comment_id,
|
||||
child_comment_total,
|
||||
level,
|
||||
parent_id,
|
||||
user_id,
|
||||
child_comment_total,
|
||||
like_total,
|
||||
first_reply_comment_id
|
||||
heat
|
||||
from t_comment
|
||||
where id in
|
||||
<foreach close=")" collection="commentIds" item="commentId" open="(" separator=",">
|
||||
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
|
||||
#{commentId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
|
||||
<insert id="batchInsert" parameterType="list">
|
||||
insert IGNORE into t_comment (id, note_id, user_id,
|
||||
content_uuid, is_content_empty, image_url,
|
||||
@@ -113,7 +120,8 @@
|
||||
is_top,
|
||||
create_time,
|
||||
first_reply_comment_id,
|
||||
child_comment_total
|
||||
child_comment_total,
|
||||
heat
|
||||
from t_comment
|
||||
where note_id = #{noteId}
|
||||
and level = 1
|
||||
@@ -128,11 +136,21 @@
|
||||
is_content_empty,
|
||||
image_url,
|
||||
like_total,
|
||||
create_time
|
||||
create_time,
|
||||
heat
|
||||
from t_comment
|
||||
where id in
|
||||
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
|
||||
#{commentId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectHeatComments" resultMap="BaseResultMap">
|
||||
select id, heat
|
||||
from t_comment
|
||||
where note_id = #{noteId}
|
||||
and level = 1
|
||||
order by heat desc
|
||||
limit 500
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user