feat(comment): 新增评论热度计算与更新功能

- 在评论数据对象中新增 childCommentTotal 和 heat 字段
- 扩展 CommentDOMapper 支持批量更新评论热度值
- 新增 CommentHeatBO 类用于封装评论热度信息
- 实现基于点赞数和回复数的热度值计算工具类 HeatCalculator
- 添加 RocketMQ 消费者异步处理评论热度更新消息
- 引入 buffer-trigger依赖实现消息聚合发送
- 扩展 JsonUtils 工具类支持 Set 类型反序列化
- 新增 MQ 常量 TOPIC_COMMENT_HEAT_UPDATE用于热度更新主题
- 修改 SQL 脚本增加 heat 字段并设置默认值- 更新测试接口请求参数内容以适配新逻辑
This commit is contained in:
2025-11-07 21:19:42 +08:00
parent 9ec330216f
commit c454e1832c
13 changed files with 272 additions and 5 deletions

View File

@@ -113,6 +113,12 @@
<artifactId>han-note-kv-api</artifactId>
</dependency>
<!-- 快手 Buffer Trigger -->
<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>buffer-trigger</artifactId>
</dependency>
</dependencies>

View File

@@ -12,4 +12,9 @@ public interface MQConstants {
*/
String TOPIC_COUNT_NOTE_COMMENT = "CountNoteCommentTopic";
/**
* Topic: 评论热度值更新
*/
String TOPIC_COMMENT_HEAT_UPDATE = "CommentHeatUpdateTopic";
}

View File

@@ -0,0 +1,90 @@
package com.hanserwei.hannote.comment.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.comment.biz.constants.MQConstants;
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
import com.hanserwei.hannote.comment.biz.domain.mapper.CommentDOMapper;
import com.hanserwei.hannote.comment.biz.model.bo.CommentHeatBO;
import com.hanserwei.hannote.comment.biz.utils.HeatCalculator;
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.stereotype.Component;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.Set;
@Component
@RocketMQMessageListener(consumerGroup = "han_note_group_" + MQConstants.TOPIC_COMMENT_HEAT_UPDATE, // Group 组
topic = MQConstants.TOPIC_COMMENT_HEAT_UPDATE // 主题 Topic
)
@Slf4j
public class CommentHeatUpdateConsumer implements RocketMQListener<String> {
@Resource
private CommentDOMapper commentDOMapper;
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
.bufferSize(50000) // 缓存队列的最大容量
.batchSize(300) // 一批次最多聚合 300 条
.linger(Duration.ofSeconds(2)) // 多久聚合一次2s 一次)
.setConsumerEx(this::consumeMessage) // 设置消费者方法
.build();
@Override
public void onMessage(String body) {
// 往 bufferTrigger 中添加元素
bufferTrigger.enqueue(body);
}
private void consumeMessage(List<String> bodys) {
log.info("==> 【评论热度值计算】聚合消息, size: {}", bodys.size());
log.info("==> 【评论热度值计算】聚合消息, {}", JsonUtils.toJsonString(bodys));
// 将聚合后的消息体 Json 转 Set<Long>, 去重相同的评论 ID, 防止重复计算
Set<Long> commentIds = Sets.newHashSet();
bodys.forEach(body -> {
try {
Set<Long> list = JsonUtils.parseSet(body, Long.class);
commentIds.addAll(list);
} catch (Exception e) {
log.error("", e);
}
});
log.info("==> 去重后的评论 ID: {}", commentIds);
// 批量查询评论
List<CommentDO> commentDOS = commentDOMapper.selectByCommentIds(commentIds.stream().toList());
// 评论 ID
List<Long> ids = Lists.newArrayList();
// 热度值 BO
List<CommentHeatBO> commentBOS = Lists.newArrayList();
//重新计算每条评论的热度值
commentDOS.forEach(commentDO -> {
Long commentId = commentDO.getId();
// 被点赞数
Long likeTotal = commentDO.getLikeTotal();
// 被回复数
Long childCommentTotal = commentDO.getChildCommentTotal();
// 计算热度值
BigDecimal heatNum = HeatCalculator.calculateHeat(likeTotal, childCommentTotal);
ids.add(commentId);
commentBOS.add(CommentHeatBO.builder()
.id(commentId)
.heat(heatNum.doubleValue())
.build());
});
// 批量更新评论热度值
commentDOMapper.batchUpdateHeatByCommentIds(ids, commentBOS);
}
}

View File

@@ -104,6 +104,12 @@ public class CommentDO {
@TableField(value = "create_time")
private LocalDateTime createTime;
/**
* 下级评论总数
*/
@TableField(value = "child_comment_total")
private Long childCommentTotal;
/**
* 更新时间
*/

View File

@@ -3,6 +3,7 @@ package com.hanserwei.hannote.comment.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
import com.hanserwei.hannote.comment.biz.model.bo.CommentBO;
import com.hanserwei.hannote.comment.biz.model.bo.CommentHeatBO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -26,4 +27,14 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
* @return 插入数量
*/
int batchInsert(@Param("comments") List<CommentBO> comments);
/**
* 批量更新热度值
*
* @param commentIds 评论 ID 列表
* @param commentHeatBOS 热度值列表
* @return 更新数量
*/
int batchUpdateHeatByCommentIds(@Param("commentIds") List<Long> commentIds,
@Param("commentHeatBOS") List<CommentHeatBO> commentHeatBOS);
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.comment.biz.model.bo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CommentHeatBO {
/**
* 评论 ID
*/
private Long id;
/**
* 热度值
*/
private Double heat;
}

View File

@@ -0,0 +1,40 @@
package com.hanserwei.hannote.comment.biz.utils;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class HeatCalculator {
// 热度计算的权重配置
private static final double LIKE_WEIGHT = 0.7; // 点赞权重 70%
private static final double REPLY_WEIGHT = 0.3; // 回复权重 30%
public static BigDecimal calculateHeat(long likeCount, long replyCount) {
// 点赞数权重 70%,被回复数权重 30%
BigDecimal likeWeight = new BigDecimal(LIKE_WEIGHT);
BigDecimal replyWeight = new BigDecimal(REPLY_WEIGHT);
// 转换点赞数和回复数为 BigDecimal
BigDecimal likeCountBD = new BigDecimal(likeCount);
BigDecimal replyCountBD = new BigDecimal(replyCount);
// 计算热度 (点赞数*点赞权重 + 回复数*回复权重)
BigDecimal heat = likeCountBD.multiply(likeWeight).add(replyCountBD.multiply(replyWeight));
// 四舍五入保留两位小数
return heat.setScale(2, RoundingMode.HALF_UP);
}
public static void main(String[] args) {
int likeCount = 150; // 点赞数
int replyCount = 10; // 被回复数
// 计算热度
BigDecimal heat = calculateHeat(likeCount, replyCount);
// 输出热度值
System.out.println("Calculated Heat: " + heat);
}
}

View File

@@ -19,18 +19,35 @@
<result column="is_top" jdbcType="TINYINT" property="isTop" />
<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"/>
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, note_id, user_id, content_uuid, is_content_empty, image_url, `level`, reply_total,
like_total, parent_id, reply_comment_id, reply_user_id, is_top, create_time, update_time
id,
note_id,
user_id,
content_uuid,
is_content_empty,
image_url,
`level`,
reply_total,
like_total,
parent_id,
reply_comment_id,
reply_user_id,
is_top,
create_time,
update_time,
child_comment_total
</sql>
<select id="selectByCommentIds" resultMap="BaseResultMap" parameterType="list">
select id,
level,
parent_id,
user_id
user_id,
child_comment_total,
like_total
from t_comment
where id in
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
@@ -52,4 +69,17 @@
, #{comment.updateTime})
</foreach>
</insert>
<update id="batchUpdateHeatByCommentIds" parameterType="map">
UPDATE t_comment
SET heat = CASE id
<foreach collection="commentHeatBOS" item="bo" separator="">
WHEN #{bo.id} THEN #{bo.heat}
</foreach>
ELSE heat END
WHERE id IN
<foreach collection="commentIds" item="commentId" open="(" close=")" separator=",">
#{commentId}
</foreach>
</update>
</mapper>

View File

@@ -7,6 +7,11 @@ public interface MQConstants {
*/
String TOPIC_COUNT_NOTE_COMMENT = "CountNoteCommentTopic";
/**
* Topic: 评论热度值更新
*/
String TOPIC_COMMENT_HEAT_UPDATE = "CommentHeatUpdateTopic";
/**
* Topic: 计数 - 笔记点赞数
*/

View File

@@ -10,14 +10,20 @@ import com.hanserwei.hannote.count.biz.enums.CommentLevelEnum;
import com.hanserwei.hannote.count.biz.model.dto.CountPublishCommentMqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@@ -27,6 +33,9 @@ import java.util.stream.Collectors;
@Slf4j
public class CountNoteChildCommentConsumer implements RocketMQListener<String> {
@Resource
private RocketMQTemplate rocketMQTemplate;
@Resource
private CommentDOMapper commentDOMapper;
@@ -76,5 +85,24 @@ public class CountNoteChildCommentConsumer implements RocketMQListener<String> {
// 更新一级评论的下级评论总数,进行累加操作
commentDOMapper.updateChildCommentTotal(parentId, count);
}
// 获取字典中所用的评论ID
Set<Long> commentIds = groupMap.keySet();
// 异步发送MQ消息计数更新评论热度值
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(commentIds))
.build();
// 异步发送 MQ 消息
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COMMENT_HEAT_UPDATE, message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("==> 【评论热度值更新】MQ 发送成功SendResult: {}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("==> 【评论热度值更新】MQ 发送异常: ", throwable);
}
});
}
}

View File

@@ -11,6 +11,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JsonUtils {
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -95,4 +96,23 @@ public class JsonUtils {
}
});
}
/**
* 将 JSON 字符串解析为指定类型的 Set 对象
*
* @param jsonStr JSON 字符串
* @param clazz 目标对象类型
* @param <T> 目标对象类型
* @return Set 集合
* @throws Exception 抛出异常
*/
public static <T> Set<T> parseSet(String jsonStr, Class<T> clazz) throws Exception {
// 使用 TypeReference 指定 Set<T> 的泛型类型
return OBJECT_MAPPER.readValue(jsonStr, new TypeReference<>() {
@Override
public CollectionType getType() {
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(Set.class, clazz);
}
});
}
}

View File

@@ -298,7 +298,7 @@ Authorization: Bearer {{token}}
{
"noteId": 1862481582414102549,
"content": "这是一条测试评论计数的二级评论333",
"content": "这是一条测试评论计数的二级评论555",
"imageUrl": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg",
"replyCommentId": 4002
}

View File

@@ -284,5 +284,9 @@ CREATE TABLE `t_comment_like`
alter table t_comment
add column `child_comment_total` bigint(20) unsigned DEFAULT '0' COMMENT '二级评论总数(只有一级评论才需要统计)';
ALTER TABLE t_comment
ADD COLUMN heat DECIMAL(10, 2) DEFAULT 0 COMMENT '评论热度';