From 6fbe8eed2515fc65abd7ba0945b921a12a3f0880 Mon Sep 17 00:00:00 2001
From: Hanserwei <2628273921@qq.com>
Date: Sat, 8 Nov 2025 11:43:32 +0800
Subject: [PATCH] =?UTF-8?q?feat(comment):=20=E5=AE=9E=E7=8E=B0=E8=AF=84?=
=?UTF-8?q?=E8=AE=BA=E7=83=AD=E5=BA=A6=E6=8E=92=E5=BA=8F=E5=8F=8A=E7=BC=93?=
=?UTF-8?q?=E5=AD=98=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修改 CommentDO 中 heat 字段类型从 BigDecimal为 Double
- 新增 selectHeatComments 方法用于查询热门评论- 优化评论分页查询逻辑,引入 Redis 缓存提升性能
- 新增评论总数与热门评论的 Redis 缓存同步机制
- 实现评论详情的 Redis 批量缓存与过期策略
- 添加 COMMENT_NOT_FOUND 业务异常码
- 更新 RedisKeyConstants 增加相关键构建方法
- 调整 XML 映射文件以支持新的查询与字段类型- 引入 RedisTemplate 和线程池异步处理缓存操作
- 在 FindCommentItemRspVO 中新增 heat 字段返回热度值
---
.idea/dictionaries/project.xml | 1 +
.../biz/constants/RedisKeyConstants.java | 50 +++
.../biz/domain/dataobject/CommentDO.java | 3 +-
.../biz/domain/mapper/CommentDOMapper.java | 8 +
.../comment/biz/enums/ResponseCodeEnum.java | 1 +
.../biz/model/vo/FindCommentItemRspVO.java | 5 +
.../biz/service/impl/CommentServiceImpl.java | 390 +++++++++++++-----
.../resources/mapperxml/CommentDOMapper.xml | 36 +-
8 files changed, 380 insertions(+), 114 deletions(-)
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 70f0f46..5bc28bd 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -6,6 +6,7 @@
hannote
hanserwei
jobhandler
+ mget
nacos
operationlog
rustfs
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java
index 1687bf4..0b3fa91 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java
@@ -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;
+ }
+
}
\ No newline at end of file
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java
index 6ac1db6..2f0591d 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java
@@ -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 (只有一级评论需要)
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java
index d1e5327..e3cc5ef 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java
@@ -74,4 +74,12 @@ public interface CommentDOMapper extends BaseMapper {
* @return 二级评论
*/
List selectTwoLevelCommentByIds(@Param("commentIds") List commentIds);
+
+ /**
+ * 查询热门评论
+ *
+ * @param noteId 笔记 ID
+ * @return 热门评论
+ */
+ List selectHeatComments(Long noteId);
}
\ No newline at end of file
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java
index bc19064..d042c59 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java
@@ -13,6 +13,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
PARAM_NOT_VALID("COMMENT-10001", "参数错误"),
// ----------- 业务异常状态码 -----------
+ COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"),
;
// 异常码
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java
index ed542ec..2626db3 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java
@@ -61,4 +61,9 @@ public class FindCommentItemRspVO {
*/
private FindCommentItemRspVO firstReplyComment;
+ /**
+ * 热度值
+ */
+ private Double heat;
+
}
\ No newline at end of file
diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java
index e933cb8..3abda15 100644
--- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java
+++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java
@@ -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
private KeyValueRpcService keyValueRpcService;
@Resource
private UserRpcService userRpcService;
+ @Resource
+ private RedisTemplate redisTemplate;
+ @Resource(name = "taskExecutor")
+ private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Override
public Response> publishComment(PublishCommentReqVO publishCommentReqVO) {
@@ -127,13 +137,33 @@ public class CommentServiceImpl extends ServiceImpl
// 每页展示一级评论数量
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
commentRspVOS = Lists.newArrayList();
// 计算分页查询的offset
long offset = PageResponse.getOffset(pageNo, pageSize);
- //查询一级评论
- List oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
- // 过滤出所有最早回复的二级评论ID
- List twoLevelCommentIds = oneLevelCommentIds.stream()
- .map(CommentDO::getFirstReplyCommentId)
- .filter(e -> e != 0)
- .toList();
- // 查询二级评论
- Map commentIdAndDOMap = null;
- List 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 findCommentContentReqDTOS = Lists.newArrayList();
- // 调用用户服务需要的入参
- List userIds = Lists.newArrayList();
+ // 若 ZSET 缓存存在, 并且查询的是前 50 页的评论
+ if (hasKey && offset < 500) {
+ // 使用 ZRevRange 获取某篇笔记下,按热度降序排序的一级评论 ID
+ Set