From 29cf889dd7c6a807157f537d21cfe6358247684c Mon Sep 17 00:00:00 2001 From: Hanserwei <2628273921@qq.com> Date: Fri, 7 Nov 2025 21:49:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(comment):=20=E6=96=B0=E5=A2=9E=E4=B8=80?= =?UTF-8?q?=E7=BA=A7=E8=AF=84=E8=AE=BA=E9=A6=96=E6=9D=A1=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?ID=E5=AD=97=E6=AE=B5=E5=8F=8A=E6=9B=B4=E6=96=B0=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CommentDO 中新增 firstReplyCommentId 字段,用于记录一级评论下最早回复的评论 ID - 在 CommentDOMapper 中新增 selectEarliestByParentId 和 updateFirstReplyCommentIdByPrimaryKey 方法,用于查询和更新一级评论的首条回复 ID - 在 t_comment 表中新增 first_reply_comment_id 字段- 新增 OneLevelCommentFirstReplyCommentIdUpdateConsumer 消费者,用于异步更新一级评论的首条回复 ID- 新增 RedisKeyConstants 常量类,用于构建 Redis Key - 新增 RedisTemplateConfig 配置类,用于配置 RedisTemplate - 在 pom.xml 中新增 spring-boot-starter-data-redis 依赖 --- han-note-comment/han-note-comment-biz/pom.xml | 6 + .../biz/config/RedisTemplateConfig.java | 31 ++++ .../biz/constants/RedisKeyConstants.java | 21 +++ ...mentFirstReplyCommentIdUpdateConsumer.java | 164 ++++++++++++++++++ .../biz/domain/dataobject/CommentDO.java | 6 + .../biz/domain/mapper/CommentDOMapper.java | 18 ++ .../resources/mapperxml/CommentDOMapper.xml | 22 ++- http-client/gateApi.http | 2 +- sql/createTable.sql | 4 + 9 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/config/RedisTemplateConfig.java create mode 100644 han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java create mode 100644 han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/consumer/OneLevelCommentFirstReplyCommentIdUpdateConsumer.java diff --git a/han-note-comment/han-note-comment-biz/pom.xml b/han-note-comment/han-note-comment-biz/pom.xml index 96ffe1e..2673d36 100644 --- a/han-note-comment/han-note-comment-biz/pom.xml +++ b/han-note-comment/han-note-comment-biz/pom.xml @@ -119,6 +119,12 @@ buffer-trigger + + + org.springframework.boot + spring-boot-starter-data-redis + + diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/config/RedisTemplateConfig.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/config/RedisTemplateConfig.java new file mode 100644 index 0000000..c811a55 --- /dev/null +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/config/RedisTemplateConfig.java @@ -0,0 +1,31 @@ +package com.hanserwei.hannote.comment.biz.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // 设置 RedisTemplate 的连接工厂 + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + + // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + redisTemplate.setValueSerializer(serializer); + redisTemplate.setHashValueSerializer(serializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..1687bf4 --- /dev/null +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java @@ -0,0 +1,21 @@ +package com.hanserwei.hannote.comment.biz.constants; + +public class RedisKeyConstants { + + /** + * Key 前缀:一级评论的 first_reply_comment_id 字段值是否更新标识 + */ + private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:"; + + + /** + * 构建完整 KEY + * + * @param commentId 一级评论 ID + * @return 完整 KEY + */ + public static String buildHaveFirstReplyCommentKey(Long commentId) { + return HAVE_FIRST_REPLY_COMMENT_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/consumer/OneLevelCommentFirstReplyCommentIdUpdateConsumer.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/consumer/OneLevelCommentFirstReplyCommentIdUpdateConsumer.java new file mode 100644 index 0000000..3a6597c --- /dev/null +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/consumer/OneLevelCommentFirstReplyCommentIdUpdateConsumer.java @@ -0,0 +1,164 @@ +package com.hanserwei.hannote.comment.biz.consumer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.RandomUtil; +import com.github.phantomthief.collection.BufferTrigger; +import com.google.common.collect.Lists; +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.enums.CommentLevelEnum; +import com.hanserwei.hannote.comment.biz.model.dto.CountPublishCommentMqDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Component +@RocketMQMessageListener(consumerGroup = "han_note_group_first_reply_comment_id" + MQConstants.TOPIC_COUNT_NOTE_COMMENT, // Group 组 + topic = MQConstants.TOPIC_COUNT_NOTE_COMMENT // 主题 Topic +) +@Slf4j +public class OneLevelCommentFirstReplyCommentIdUpdateConsumer implements RocketMQListener { + + @Resource + private CommentDOMapper commentDOMapper; + @Resource(name = "taskExecutor") + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Resource + private RedisTemplate redisTemplate; + + private final BufferTrigger bufferTrigger = BufferTrigger.batchBlocking() + .bufferSize(50000) // 缓存队列的最大容量 + .batchSize(1000) // 一批次最多聚合 1000 条 + .linger(Duration.ofSeconds(1)) // 多久聚合一次(1s 一次) + .setConsumerEx(this::consumeMessage) // 设置消费者方法 + .build(); + + @Override + public void onMessage(String body) { + // 往 bufferTrigger 中添加元素 + bufferTrigger.enqueue(body); + } + + private void consumeMessage(List bodys) { + log.info("==> 【一级评论 first_reply_comment_id 更新】聚合消息, size: {}", bodys.size()); + log.info("==> 【一级评论 first_reply_comment_id 更新】聚合消息, {}", JsonUtils.toJsonString(bodys)); + + // 将聚合后的消息体 Json 转 List + List publishCommentMqDTOS = Lists.newArrayList(); + bodys.forEach(body -> { + try { + List list = JsonUtils.parseList(body, CountPublishCommentMqDTO.class); + publishCommentMqDTOS.addAll(list); + } catch (Exception e) { + log.error("", e); + } + }); + + // 过滤出二级评论的 parent_id(即一级评论 ID),并去重,需要更新对应一级评论的 first_reply_comment_id + List parentIds = publishCommentMqDTOS.stream() + .filter(publishCommentMqDTO -> Objects.equals(publishCommentMqDTO.getLevel(), CommentLevelEnum.TWO.getCode())) + .map(CountPublishCommentMqDTO::getParentId) + .distinct() // 去重 + .toList(); + + if (CollUtil.isEmpty(parentIds)) return; + + // 构建RedisKey + List keys = parentIds.stream() + .map(RedisKeyConstants::buildHaveFirstReplyCommentKey) + .toList(); + // 批量查询Redis + List values = redisTemplate.opsForValue().multiGet(keys); + + // 提取Redis中不存在的评论ID + List missingCommentIds = Lists.newArrayList(); + + if (values != null) { + for (int i = 0; i < values.size(); i++) { + if (Objects.isNull(values.get(i))) { + missingCommentIds.add(parentIds.get(i)); + } + } + } + + // 存在的一级评论 ID,说明表中对应记录的 first_reply_comment_id 已经有值 + if (CollUtil.isNotEmpty(missingCommentIds)) { + // 不存在的,则需要进一步查询数据库来确定,是否要更新记录对应的 first_reply_comment_id 值 + // 批量去数据库中查询 + List commentDOS = commentDOMapper.selectByCommentIds(missingCommentIds); + + // 异步将 first_reply_comment_id 不为 0 的一级评论 ID, 同步到 redis 中 + threadPoolTaskExecutor.submit(() -> { + List needSyncCommentIds = commentDOS.stream() + .filter(commentDO -> commentDO.getFirstReplyCommentId() != 0) + .map(CommentDO::getId) + .toList(); + + sync2Redis(needSyncCommentIds); + }); + + // 过滤出值为 0 的,都需要更新其 first_reply_comment_id + List needUpdateCommentDOS = commentDOS.stream() + .filter(commentDO -> commentDO.getFirstReplyCommentId() == 0) + .toList(); + + needUpdateCommentDOS.forEach(needUpdateCommentDO -> { + // 一级评论 ID + Long needUpdateCommentId = needUpdateCommentDO.getId(); + + // 查询数据库,拿到一级评论最早回复的那条评论 + CommentDO earliestCommentDO = commentDOMapper.selectEarliestByParentId(needUpdateCommentId); + + if (Objects.nonNull(earliestCommentDO)) { + // 最早回复的那条评论 ID + Long earliestCommentId = earliestCommentDO.getId(); + + // 更新其一级评论的 first_reply_comment_id + commentDOMapper.updateFirstReplyCommentIdByPrimaryKey(earliestCommentId, needUpdateCommentId); + + // 异步同步到 Redis + threadPoolTaskExecutor.submit(() -> sync2Redis(Lists.newArrayList(needUpdateCommentId))); + } + }); + + } + } + + /** + * 异步将 first_reply_comment_id 不为 0 的一级评论 ID, 同步到 redis 中 + * + * @param needSyncCommentIds 需要同步的评论 ID + */ + private void sync2Redis(List needSyncCommentIds) { + // 获取 ValueOperations + ValueOperations valueOperations = redisTemplate.opsForValue(); + + // 使用 RedisTemplate 的管道模式,允许在一个操作中批量发送多个命令,防止频繁操作 Redis + redisTemplate.executePipelined((RedisCallback) (connection) -> { + needSyncCommentIds.forEach(needSyncCommentId -> { + // 构建 Redis Key + String key = RedisKeyConstants.buildHaveFirstReplyCommentKey(needSyncCommentId); + + // 批量设置值并指定过期时间(5小时以内) + valueOperations.set(key, 1, RandomUtil.randomInt(5 * 60 * 60), TimeUnit.SECONDS); + }); + return null; + }); + } + +} \ 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 ef05284..cb451d4 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 @@ -115,4 +115,10 @@ public class CommentDO { */ @TableField(value = "update_time") private LocalDateTime updateTime; + + /** + * 一级评论的第一个回复的评论ID + */ + @TableField(value = "first_reply_comment_id") + private Long firstReplyCommentId; } \ 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/mapper/CommentDOMapper.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java index 9c09da1..fc73840 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 @@ -37,4 +37,22 @@ public interface CommentDOMapper extends BaseMapper { */ int batchUpdateHeatByCommentIds(@Param("commentIds") List commentIds, @Param("commentHeatBOS") List commentHeatBOS); + + /** + * 查询一级评论下最早回复的评论 + * + * @param parentId 一级评论 ID + * @return 一级评论下最早回复的评论 + */ + CommentDO selectEarliestByParentId(Long parentId); + + /** + * 更新一级评论的 first_reply_comment_id + * + * @param firstReplyCommentId 一级评论下最早回复的评论 ID + * @param id 一级评论 ID + * @return 更新数量 + */ + int updateFirstReplyCommentIdByPrimaryKey(@Param("firstReplyCommentId") Long firstReplyCommentId, + @Param("id") Long id); } \ No newline at end of file diff --git a/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml b/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml index eb904bc..9cf11d3 100644 --- a/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml +++ b/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml @@ -20,6 +20,7 @@ + @@ -38,7 +39,8 @@ is_top, create_time, update_time, - child_comment_total + child_comment_total, + first_reply_comment_id + select id + from t_comment + where parent_id = #{parentId} + and level = 2 + order by create_time + limit 1 + + + + update t_comment + set first_reply_comment_id = #{firstReplyCommentId} + where id = #{id} + \ No newline at end of file diff --git a/http-client/gateApi.http b/http-client/gateApi.http index 1951fe4..8378bd9 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -298,7 +298,7 @@ Authorization: Bearer {{token}} { "noteId": 1862481582414102549, - "content": "这是一条测试评论计数的二级评论555", + "content": "这是一条测试评论计数的二级评论666", "imageUrl": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg", "replyCommentId": 4002 } diff --git a/sql/createTable.sql b/sql/createTable.sql index a295237..6be0d9e 100644 --- a/sql/createTable.sql +++ b/sql/createTable.sql @@ -287,6 +287,10 @@ alter table t_comment ALTER TABLE t_comment ADD COLUMN heat DECIMAL(10, 2) DEFAULT 0 COMMENT '评论热度'; +alter table t_comment + add column first_reply_comment_id bigint(20) unsigned default 0 COMMENT '最早回复的评论ID (只有一级评论需要)'; + +