feat(count): 新增笔记收藏与点赞计数聚合功能,用户维度统计功能

- 新增 AggregationCountCollectedUncollectedNoteMqDTO 和 AggregationCountLikeUnlikeNoteMqDTO 聚合消息体
- 在 CollectUnCollectNoteMqDTO、CountCollectUnCollectNoteMqDTO 和 CountLikeUnlikeNoteMqDTO 中添加 noteCreatorId 字段
- 优化 CountNoteCollect2DBConsumer 和 CountNoteLike2DBConsumer 消费者逻辑,支持事务性更新用户及笔记计数
- 修改 CountNoteCollectConsumer 和 CountNoteLikeConsumer,使用聚合 DTO 替代 Map 结构处理计数逻辑
- 扩展 JsonUtils 工具类,新增 parseList 方法用于解析 JSON 到 List 对象
- 更新 NoteServiceImpl 中点赞和收藏相关方法,补充获取并传递 noteCreatorId 参数
- 在 UserCountDOMapper 及其 XML 映射文件中新增点赞数和收藏数的插入或更新操作接口
This commit is contained in:
2025-10-19 17:18:20 +08:00
parent 564eefa7bc
commit 7b1df60c05
14 changed files with 295 additions and 54 deletions

View File

@@ -5,13 +5,16 @@ import com.google.common.util.concurrent.RateLimiter;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.domain.mapper.NoteCountDOMapper;
import com.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountCollectedUncollectedNoteMqDTO;
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 org.springframework.transaction.support.TransactionTemplate;
import java.util.Map;
import java.util.List;
@SuppressWarnings("UnstableApiUsage")
@Component
@@ -26,6 +29,10 @@ public class CountNoteCollect2DBConsumer implements RocketMQListener<String> {
private final RateLimiter rateLimiter = RateLimiter.create(5000);
@Resource
private NoteCountDOMapper noteCountDOMapper;
@Resource
private UserCountDOMapper userCountDOMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override
public void onMessage(String body) {
@@ -34,16 +41,32 @@ public class CountNoteCollect2DBConsumer implements RocketMQListener<String> {
log.info("## 消费到了 MQ 【计数: 笔记收藏数入库】, {}...", body);
Map<Long, Integer> countMap = null;
List<AggregationCountCollectedUncollectedNoteMqDTO> countList = null;
try {
countMap = JsonUtils.parseMap(body, Long.class, Integer.class);
countList = JsonUtils.parseList(body, AggregationCountCollectedUncollectedNoteMqDTO.class);
} catch (Exception e) {
log.error("## 解析 JSON 字符串异常", e);
log.error("## 解析 JSON 字符串异常");
}
if (CollUtil.isNotEmpty(countMap)) {
// 判断数据库中 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新
countMap.forEach((k, v) -> noteCountDOMapper.insertOrUpdateCollectTotalByNoteId(v, k));
if (CollUtil.isNotEmpty(countList)) {
countList.forEach(item -> {
Long creatorId = item.getCreatorId();
Long noteId = item.getNoteId();
Integer count = item.getCount();
// 编程式事务,保证两条语句的原子性
transactionTemplate.execute(status -> {
try {
noteCountDOMapper.insertOrUpdateCollectTotalByNoteId(count, noteId);
userCountDOMapper.insertOrUpdateCollectTotalByUserId(count, creatorId);
return true;
} catch (Exception ex) {
status.setRollbackOnly(); // 标记事务为回滚
log.error("", ex);
}
return false;
});
});
}
}
}

View File

@@ -1,11 +1,12 @@
package com.hanserwei.hannote.count.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger;
import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.count.biz.enums.CollectUnCollectNoteTypeEnum;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountCollectedUncollectedNoteMqDTO;
import com.hanserwei.hannote.count.biz.model.dto.CountCollectUnCollectNoteMqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -63,14 +64,18 @@ public class CountNoteCollectConsumer implements RocketMQListener<String> {
Map<Long, List<CountCollectUnCollectNoteMqDTO>> groupMap = countCollectUnCollectNoteMqDTOS.stream()
.collect(Collectors.groupingBy(CountCollectUnCollectNoteMqDTO::getNoteId));
// 按组汇总数据,统计出最终的计数
// key 为笔记 ID, value 为最终操作的计数
Map<Long, Integer> countMap = Maps.newHashMap();
List<AggregationCountCollectedUncollectedNoteMqDTO> countList = Lists.newArrayList();
for (Map.Entry<Long, List<CountCollectUnCollectNoteMqDTO>> entry : groupMap.entrySet()) {
// 笔记 ID
Long noteId = entry.getKey();
// 笔记发布者 ID
Long creatorId = null;
List<CountCollectUnCollectNoteMqDTO> list = entry.getValue();
// 默认计数为0
int finalCount = 0;
for (CountCollectUnCollectNoteMqDTO countCollectUnCollectNoteMqDTO : list) {
Integer type = countCollectUnCollectNoteMqDTO.getType();
creatorId = countCollectUnCollectNoteMqDTO.getNoteCreatorId();
// 获取枚举类
CollectUnCollectNoteTypeEnum collectUnCollectNoteTypeEnum = CollectUnCollectNoteTypeEnum.valueOf(type);
switch (Objects.requireNonNull(collectUnCollectNoteTypeEnum)) {
@@ -78,28 +83,46 @@ public class CountNoteCollectConsumer implements RocketMQListener<String> {
case UN_COLLECT -> finalCount--;
}
}
// 将分组后统计出的最终计数,存入 countMap
countMap.put(entry.getKey(), finalCount);
// 将分组后统计出的最终计数,存入 countList
countList.add(AggregationCountCollectedUncollectedNoteMqDTO.builder()
.noteId(noteId)
.creatorId(creatorId)
.count(finalCount)
.build());
}
log.info("==> 【笔记收藏数】最终结果, {}", JsonUtils.toJsonString(countMap));
log.info("==> 【笔记收藏数】最终结果, {}", JsonUtils.toJsonString(countList));
// 更新 Redis
countMap.forEach((k, v) -> {
// Redis Hash Key
String redisKey = RedisKeyConstants.buildCountNoteKey(k);
// 判断 Redis 中 Hash 是否存在
boolean isExisted = redisTemplate.hasKey(redisKey);
countList.forEach(item -> {
// 笔记发布者 ID
Long creatorId = item.getCreatorId();
// 笔记 ID
Long noteId = item.getNoteId();
// 聚合后的计数
Integer count = item.getCount();
// 笔记维度计数 Redis Key
String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
// 判断Redis 中 Hash 是否存在
boolean isCountNoteExisted = redisTemplate.hasKey(countNoteRedisKey);
// 若存在才会更新
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
if (isExisted) {
// 对目标用户 Hash 中的收藏总数字段进行计数操作
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL, v);
if (isCountNoteExisted) {
// 对目标用户 Hash 中的点赞数字段进行计数操作
redisTemplate.opsForHash().increment(countNoteRedisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL, count);
}
// 更新 Redis 用户维度收藏数
String countUserRedisKey = RedisKeyConstants.buildCountUserKey(creatorId);
Boolean isCountUserExisted = redisTemplate.hasKey(countUserRedisKey);
if (isCountUserExisted) {
// 对目标用户 Hash 中的收藏数字段进行计数操作
redisTemplate.opsForHash().increment(countUserRedisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL, count);
}
});
// 发送 MQ, 笔记收藏数据落库
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countMap))
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countList))
.build();
// 异步发送 MQ 消息

View File

@@ -5,13 +5,16 @@ import com.google.common.util.concurrent.RateLimiter;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.domain.mapper.NoteCountDOMapper;
import com.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountLikeUnlikeNoteMqDTO;
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 org.springframework.transaction.support.TransactionTemplate;
import java.util.Map;
import java.util.List;
@Component
@Slf4j
@@ -26,6 +29,10 @@ public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
private final RateLimiter rateLimiter = RateLimiter.create(5000);
@Resource
private NoteCountDOMapper noteCountDOMapper;
@Resource
private UserCountDOMapper userCountDOMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override
public void onMessage(String body) {
@@ -34,16 +41,33 @@ public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
log.info("## 消费到了 MQ 【计数: 笔记点赞数入库】, {}...", body);
Map<Long, Integer> countMap = null;
List<AggregationCountLikeUnlikeNoteMqDTO> countList = null;
try {
countMap = JsonUtils.parseMap(body, Long.class, Integer.class);
countList = JsonUtils.parseList(body, AggregationCountLikeUnlikeNoteMqDTO.class);
} catch (Exception e) {
log.error("## 解析 JSON 字符串异常", e);
}
if (CollUtil.isNotEmpty(countMap)) {
// 判断数据库中 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新
countMap.forEach((k, v) -> noteCountDOMapper.insertOrUpdateLikeTotalByNoteId(v, k));
if (CollUtil.isNotEmpty(countList)) {
// 判断数据库中 t_user_count 和 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新
countList.forEach(item -> {
Long creatorId = item.getCreatorId();
Long noteId = item.getNoteId();
Integer count = item.getCount();
// 编程式事务,保证两条语句的原子性
transactionTemplate.execute(status -> {
try {
noteCountDOMapper.insertOrUpdateLikeTotalByNoteId(count, noteId);
userCountDOMapper.insertOrUpdateLikeTotalByUserId(count, creatorId);
return true;
} catch (Exception ex) {
status.setRollbackOnly(); // 标记事务为回滚
log.error("", ex);
}
return false;
});
});
}
}
}

View File

@@ -1,11 +1,12 @@
package com.hanserwei.hannote.count.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger;
import com.google.common.collect.Maps;
import com.google.common.collect.Lists;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.count.biz.enums.LikeUnlikeNoteTypeEnum;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountLikeUnlikeNoteMqDTO;
import com.hanserwei.hannote.count.biz.model.dto.CountLikeUnlikeNoteMqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -60,13 +61,18 @@ public class CountNoteLikeConsumer implements RocketMQListener<String> {
.collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId));
// 按组汇总统计处最终计数
// key为笔记IDvalue为最终操作计数
Map<Long, Integer> countMap = Maps.newHashMap();
List<AggregationCountLikeUnlikeNoteMqDTO> countList = Lists.newArrayList();
for (Map.Entry<Long, List<CountLikeUnlikeNoteMqDTO>> entry : groupMap.entrySet()) {
// 笔记 ID
Long noteId = entry.getKey();
// 笔记发布者 ID
Long creatorId = null;
List<CountLikeUnlikeNoteMqDTO> list = entry.getValue();
// 最终计数默认为0
// 最终计数值,默认为 0
int finalCount = 0;
for (CountLikeUnlikeNoteMqDTO countLikeUnlikeNoteMqDTO : list) {
// 设置笔记发布者用户 ID
creatorId = countLikeUnlikeNoteMqDTO.getNoteCreatorId();
Integer type = countLikeUnlikeNoteMqDTO.getType();
LikeUnlikeNoteTypeEnum likeUnlikeNoteTypeEnum = LikeUnlikeNoteTypeEnum.valueOf(type);
if (likeUnlikeNoteTypeEnum == null) {
@@ -77,26 +83,45 @@ public class CountNoteLikeConsumer implements RocketMQListener<String> {
case UNLIKE -> finalCount--;
}
}
countMap.put(entry.getKey(), finalCount);
// 将分组后统计出的最终计数,存入 countList 中
countList.add(AggregationCountLikeUnlikeNoteMqDTO.builder()
.noteId(noteId)
.creatorId(creatorId)
.count(finalCount)
.build());
}
log.info("## 【笔记点赞数】聚合后的计数数据: {}", JsonUtils.toJsonString(countMap));
// 更新Redis
countMap.forEach((k, v) -> {
// Redis Key
String redisKey = RedisKeyConstants.buildCountNoteKey(k);
log.info("## 【笔记点赞数】聚合后的计数数据: {}", JsonUtils.toJsonString(countList));
// 更新 Redis
countList.forEach(item -> {
// 笔记发布者 ID
Long creatorId = item.getCreatorId();
// 笔记 ID
Long noteId = item.getNoteId();
// 聚合后的计数
Integer count = item.getCount();
// 笔记维度计数 Redis Key
String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
// 判断 Redis 中 Hash 是否存在
boolean isExisted = redisTemplate.hasKey(redisKey);
boolean isCountNoteExisted = redisTemplate.hasKey(countNoteRedisKey);
// 若存在才会更新
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
if (isExisted) {
if (isCountNoteExisted) {
// 对目标用户 Hash 中的点赞数字段进行计数操作
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_LIKE_TOTAL, v);
redisTemplate.opsForHash().increment(countNoteRedisKey, RedisKeyConstants.FIELD_LIKE_TOTAL, count);
}
// 更新 Redis 用户维度点赞数
String countUserRedisKey = RedisKeyConstants.buildCountUserKey(creatorId);
boolean isCountUserExisted = redisTemplate.hasKey(countUserRedisKey);
if (isCountUserExisted) {
redisTemplate.opsForHash().increment(countUserRedisKey, RedisKeyConstants.FIELD_LIKE_TOTAL, count);
}
});
// 发送 MQ, 笔记点赞数据落库
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countMap))
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countList))
.build();
// 异步发送 MQ 消息

View File

@@ -25,4 +25,22 @@ public interface UserCountDOMapper extends BaseMapper<UserCountDO> {
* @return 影响行数
*/
int insertOrUpdateFollowingTotalByUserId(@Param("count") Integer count, @Param("userId") Long userId);
/**
* 添加记录或更新笔记点赞数
*
* @param count 点赞数
* @param userId 用户ID
* @return 影响行数
*/
int insertOrUpdateLikeTotalByUserId(@Param("count") Integer count, @Param("userId") Long userId);
/**
* 添加记录或更新笔记收藏数
*
* @param count 收藏数
* @param userId 用户ID
* @return 影响行数
*/
int insertOrUpdateCollectTotalByUserId(@Param("count") Integer count, @Param("userId") Long userId);
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.hannote.count.biz.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 聚合后的收藏、取消收藏笔记消息体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AggregationCountCollectedUncollectedNoteMqDTO {
/**
* 笔记发布者 ID
*/
private Long creatorId;
/**
* 笔记 ID
*/
private Long noteId;
/**
* 聚合后的计数
*/
private Integer count;
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.hannote.count.biz.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 聚合后的点赞、取消点赞笔记消息体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AggregationCountLikeUnlikeNoteMqDTO {
/**
* 笔记发布者 ID
*/
private Long creatorId;
/**
* 笔记 ID
*/
private Long noteId;
/**
* 聚合后的计数
*/
private Integer count;
}

View File

@@ -23,4 +23,9 @@ public class CountCollectUnCollectNoteMqDTO {
private Integer type;
private LocalDateTime createTime;
/**
* 笔记发布者 ID
*/
private Long noteCreatorId;
}

View File

@@ -23,4 +23,9 @@ public class CountLikeUnlikeNoteMqDTO {
private Integer type;
private LocalDateTime createTime;
/**
* 笔记发布者 ID
*/
private Long noteCreatorId;
}

View File

@@ -28,4 +28,16 @@
VALUES (#{userId}, #{count})
ON DUPLICATE KEY UPDATE following_total = following_total + (#{count});
</insert>
<insert id="insertOrUpdateLikeTotalByUserId" parameterType="map">
INSERT INTO t_user_count (user_id, like_total)
VALUES (#{userId}, #{count})
ON DUPLICATE KEY UPDATE like_total = like_total + (#{count});
</insert>
<insert id="insertOrUpdateCollectTotalByUserId" parameterType="map">
INSERT INTO t_user_count (user_id, collect_total)
VALUES (#{userId}, #{count})
ON DUPLICATE KEY UPDATE collect_total = collect_total + (#{count});
</insert>
</mapper>