feat(comment): 新增评论热度计算与更新功能
- 在评论数据对象中新增 childCommentTotal 和 heat 字段 - 扩展 CommentDOMapper 支持批量更新评论热度值 - 新增 CommentHeatBO 类用于封装评论热度信息 - 实现基于点赞数和回复数的热度值计算工具类 HeatCalculator - 添加 RocketMQ 消费者异步处理评论热度更新消息 - 引入 buffer-trigger依赖实现消息聚合发送 - 扩展 JsonUtils 工具类支持 Set 类型反序列化 - 新增 MQ 常量 TOPIC_COMMENT_HEAT_UPDATE用于热度更新主题 - 修改 SQL 脚本增加 heat 字段并设置默认值- 更新测试接口请求参数内容以适配新逻辑
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -12,4 +12,9 @@ public interface MQConstants {
|
||||
*/
|
||||
String TOPIC_COUNT_NOTE_COMMENT = "CountNoteCommentTopic";
|
||||
|
||||
/**
|
||||
* Topic: 评论热度值更新
|
||||
*/
|
||||
String TOPIC_COMMENT_HEAT_UPDATE = "CommentHeatUpdateTopic";
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,12 @@ public class CommentDO {
|
||||
@TableField(value = "create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 下级评论总数
|
||||
*/
|
||||
@TableField(value = "child_comment_total")
|
||||
private Long childCommentTotal;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
<!--@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,
|
||||
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>
|
||||
@@ -7,6 +7,11 @@ public interface MQConstants {
|
||||
*/
|
||||
String TOPIC_COUNT_NOTE_COMMENT = "CountNoteCommentTopic";
|
||||
|
||||
/**
|
||||
* Topic: 评论热度值更新
|
||||
*/
|
||||
String TOPIC_COMMENT_HEAT_UPDATE = "CommentHeatUpdateTopic";
|
||||
|
||||
/**
|
||||
* Topic: 计数 - 笔记点赞数
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 '评论热度';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user