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.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants; 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.NoteCountDOMapper;
import com.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountCollectedUncollectedNoteMqDTO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener; import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.Map; import java.util.List;
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
@Component @Component
@@ -26,6 +29,10 @@ public class CountNoteCollect2DBConsumer implements RocketMQListener<String> {
private final RateLimiter rateLimiter = RateLimiter.create(5000); private final RateLimiter rateLimiter = RateLimiter.create(5000);
@Resource @Resource
private NoteCountDOMapper noteCountDOMapper; private NoteCountDOMapper noteCountDOMapper;
@Resource
private UserCountDOMapper userCountDOMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override @Override
public void onMessage(String body) { public void onMessage(String body) {
@@ -34,16 +41,32 @@ public class CountNoteCollect2DBConsumer implements RocketMQListener<String> {
log.info("## 消费到了 MQ 【计数: 笔记收藏数入库】, {}...", body); log.info("## 消费到了 MQ 【计数: 笔记收藏数入库】, {}...", body);
Map<Long, Integer> countMap = null; List<AggregationCountCollectedUncollectedNoteMqDTO> countList = null;
try { try {
countMap = JsonUtils.parseMap(body, Long.class, Integer.class); countList = JsonUtils.parseList(body, AggregationCountCollectedUncollectedNoteMqDTO.class);
} catch (Exception e) { } catch (Exception e) {
log.error("## 解析 JSON 字符串异常", e); log.error("## 解析 JSON 字符串异常");
} }
if (CollUtil.isNotEmpty(countMap)) { if (CollUtil.isNotEmpty(countList)) {
// 判断数据库中 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新 countList.forEach(item -> {
countMap.forEach((k, v) -> noteCountDOMapper.insertOrUpdateCollectTotalByNoteId(v, k)); 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; package com.hanserwei.hannote.count.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger; 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.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants; import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants; import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.count.biz.enums.CollectUnCollectNoteTypeEnum; 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 com.hanserwei.hannote.count.biz.model.dto.CountCollectUnCollectNoteMqDTO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -63,14 +64,18 @@ public class CountNoteCollectConsumer implements RocketMQListener<String> {
Map<Long, List<CountCollectUnCollectNoteMqDTO>> groupMap = countCollectUnCollectNoteMqDTOS.stream() Map<Long, List<CountCollectUnCollectNoteMqDTO>> groupMap = countCollectUnCollectNoteMqDTOS.stream()
.collect(Collectors.groupingBy(CountCollectUnCollectNoteMqDTO::getNoteId)); .collect(Collectors.groupingBy(CountCollectUnCollectNoteMqDTO::getNoteId));
// 按组汇总数据,统计出最终的计数 // 按组汇总数据,统计出最终的计数
// key 为笔记 ID, value 为最终操作的计数 List<AggregationCountCollectedUncollectedNoteMqDTO> countList = Lists.newArrayList();
Map<Long, Integer> countMap = Maps.newHashMap();
for (Map.Entry<Long, List<CountCollectUnCollectNoteMqDTO>> entry : groupMap.entrySet()) { for (Map.Entry<Long, List<CountCollectUnCollectNoteMqDTO>> entry : groupMap.entrySet()) {
// 笔记 ID
Long noteId = entry.getKey();
// 笔记发布者 ID
Long creatorId = null;
List<CountCollectUnCollectNoteMqDTO> list = entry.getValue(); List<CountCollectUnCollectNoteMqDTO> list = entry.getValue();
// 默认计数为0 // 默认计数为0
int finalCount = 0; int finalCount = 0;
for (CountCollectUnCollectNoteMqDTO countCollectUnCollectNoteMqDTO : list) { for (CountCollectUnCollectNoteMqDTO countCollectUnCollectNoteMqDTO : list) {
Integer type = countCollectUnCollectNoteMqDTO.getType(); Integer type = countCollectUnCollectNoteMqDTO.getType();
creatorId = countCollectUnCollectNoteMqDTO.getNoteCreatorId();
// 获取枚举类 // 获取枚举类
CollectUnCollectNoteTypeEnum collectUnCollectNoteTypeEnum = CollectUnCollectNoteTypeEnum.valueOf(type); CollectUnCollectNoteTypeEnum collectUnCollectNoteTypeEnum = CollectUnCollectNoteTypeEnum.valueOf(type);
switch (Objects.requireNonNull(collectUnCollectNoteTypeEnum)) { switch (Objects.requireNonNull(collectUnCollectNoteTypeEnum)) {
@@ -78,28 +83,46 @@ public class CountNoteCollectConsumer implements RocketMQListener<String> {
case UN_COLLECT -> finalCount--; case UN_COLLECT -> finalCount--;
} }
} }
// 将分组后统计出的最终计数,存入 countMap // 将分组后统计出的最终计数,存入 countList
countMap.put(entry.getKey(), finalCount); countList.add(AggregationCountCollectedUncollectedNoteMqDTO.builder()
.noteId(noteId)
.creatorId(creatorId)
.count(finalCount)
.build());
} }
log.info("==> 【笔记收藏数】最终结果, {}", JsonUtils.toJsonString(countMap)); log.info("==> 【笔记收藏数】最终结果, {}", JsonUtils.toJsonString(countList));
// 更新 Redis // 更新 Redis
countMap.forEach((k, v) -> { countList.forEach(item -> {
// Redis Hash Key // 笔记发布者 ID
String redisKey = RedisKeyConstants.buildCountNoteKey(k); Long creatorId = item.getCreatorId();
// 判断 Redis 中 Hash 是否存在 // 笔记 ID
boolean isExisted = redisTemplate.hasKey(redisKey); Long noteId = item.getNoteId();
// 聚合后的计数
Integer count = item.getCount();
// 笔记维度计数 Redis Key
String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
// 判断Redis 中 Hash 是否存在
boolean isCountNoteExisted = redisTemplate.hasKey(countNoteRedisKey);
// 若存在才会更新 // 若存在才会更新
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做) // (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
if (isExisted) { if (isCountNoteExisted) {
// 对目标用户 Hash 中的收藏总数字段进行计数操作 // 对目标用户 Hash 中的点赞数字段进行计数操作
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_COLLECT_TOTAL, v); 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, 笔记收藏数据落库 // 发送 MQ, 笔记收藏数据落库
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countMap)) Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countList))
.build(); .build();
// 异步发送 MQ 消息 // 异步发送 MQ 消息

View File

@@ -5,13 +5,16 @@ import com.google.common.util.concurrent.RateLimiter;
import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants; 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.NoteCountDOMapper;
import com.hanserwei.hannote.count.biz.domain.mapper.UserCountDOMapper;
import com.hanserwei.hannote.count.biz.model.dto.AggregationCountLikeUnlikeNoteMqDTO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener; import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.Map; import java.util.List;
@Component @Component
@Slf4j @Slf4j
@@ -26,6 +29,10 @@ public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
private final RateLimiter rateLimiter = RateLimiter.create(5000); private final RateLimiter rateLimiter = RateLimiter.create(5000);
@Resource @Resource
private NoteCountDOMapper noteCountDOMapper; private NoteCountDOMapper noteCountDOMapper;
@Resource
private UserCountDOMapper userCountDOMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Override @Override
public void onMessage(String body) { public void onMessage(String body) {
@@ -34,16 +41,33 @@ public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
log.info("## 消费到了 MQ 【计数: 笔记点赞数入库】, {}...", body); log.info("## 消费到了 MQ 【计数: 笔记点赞数入库】, {}...", body);
Map<Long, Integer> countMap = null; List<AggregationCountLikeUnlikeNoteMqDTO> countList = null;
try { try {
countMap = JsonUtils.parseMap(body, Long.class, Integer.class); countList = JsonUtils.parseList(body, AggregationCountLikeUnlikeNoteMqDTO.class);
} catch (Exception e) { } catch (Exception e) {
log.error("## 解析 JSON 字符串异常", e); log.error("## 解析 JSON 字符串异常", e);
} }
if (CollUtil.isNotEmpty(countMap)) { if (CollUtil.isNotEmpty(countList)) {
// 判断数据库中 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新 // 判断数据库中 t_user_count 和 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新
countMap.forEach((k, v) -> noteCountDOMapper.insertOrUpdateLikeTotalByNoteId(v, k)); 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; package com.hanserwei.hannote.count.biz.consumer;
import com.github.phantomthief.collection.BufferTrigger; 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.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.count.biz.constant.MQConstants; import com.hanserwei.hannote.count.biz.constant.MQConstants;
import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants; import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants;
import com.hanserwei.hannote.count.biz.enums.LikeUnlikeNoteTypeEnum; 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 com.hanserwei.hannote.count.biz.model.dto.CountLikeUnlikeNoteMqDTO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -60,13 +61,18 @@ public class CountNoteLikeConsumer implements RocketMQListener<String> {
.collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId)); .collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId));
// 按组汇总统计处最终计数 // 按组汇总统计处最终计数
// key为笔记IDvalue为最终操作计数 List<AggregationCountLikeUnlikeNoteMqDTO> countList = Lists.newArrayList();
Map<Long, Integer> countMap = Maps.newHashMap();
for (Map.Entry<Long, List<CountLikeUnlikeNoteMqDTO>> entry : groupMap.entrySet()) { for (Map.Entry<Long, List<CountLikeUnlikeNoteMqDTO>> entry : groupMap.entrySet()) {
// 笔记 ID
Long noteId = entry.getKey();
// 笔记发布者 ID
Long creatorId = null;
List<CountLikeUnlikeNoteMqDTO> list = entry.getValue(); List<CountLikeUnlikeNoteMqDTO> list = entry.getValue();
// 最终计数默认为0 // 最终计数值,默认为 0
int finalCount = 0; int finalCount = 0;
for (CountLikeUnlikeNoteMqDTO countLikeUnlikeNoteMqDTO : list) { for (CountLikeUnlikeNoteMqDTO countLikeUnlikeNoteMqDTO : list) {
// 设置笔记发布者用户 ID
creatorId = countLikeUnlikeNoteMqDTO.getNoteCreatorId();
Integer type = countLikeUnlikeNoteMqDTO.getType(); Integer type = countLikeUnlikeNoteMqDTO.getType();
LikeUnlikeNoteTypeEnum likeUnlikeNoteTypeEnum = LikeUnlikeNoteTypeEnum.valueOf(type); LikeUnlikeNoteTypeEnum likeUnlikeNoteTypeEnum = LikeUnlikeNoteTypeEnum.valueOf(type);
if (likeUnlikeNoteTypeEnum == null) { if (likeUnlikeNoteTypeEnum == null) {
@@ -77,26 +83,45 @@ public class CountNoteLikeConsumer implements RocketMQListener<String> {
case UNLIKE -> finalCount--; 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)); log.info("## 【笔记点赞数】聚合后的计数数据: {}", JsonUtils.toJsonString(countList));
// 更新Redis // 更新 Redis
countMap.forEach((k, v) -> { countList.forEach(item -> {
// Redis Key // 笔记发布者 ID
String redisKey = RedisKeyConstants.buildCountNoteKey(k); Long creatorId = item.getCreatorId();
// 笔记 ID
Long noteId = item.getNoteId();
// 聚合后的计数
Integer count = item.getCount();
// 笔记维度计数 Redis Key
String countNoteRedisKey = RedisKeyConstants.buildCountNoteKey(noteId);
// 判断 Redis 中 Hash 是否存在 // 判断 Redis 中 Hash 是否存在
boolean isExisted = redisTemplate.hasKey(redisKey); boolean isCountNoteExisted = redisTemplate.hasKey(countNoteRedisKey);
// 若存在才会更新 // 若存在才会更新
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做) // (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
if (isExisted) { if (isCountNoteExisted) {
// 对目标用户 Hash 中的点赞数字段进行计数操作 // 对目标用户 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, 笔记点赞数据落库 // 发送 MQ, 笔记点赞数据落库
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countMap)) Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countList))
.build(); .build();
// 异步发送 MQ 消息 // 异步发送 MQ 消息

View File

@@ -25,4 +25,22 @@ public interface UserCountDOMapper extends BaseMapper<UserCountDO> {
* @return 影响行数 * @return 影响行数
*/ */
int insertOrUpdateFollowingTotalByUserId(@Param("count") Integer count, @Param("userId") Long userId); 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 Integer type;
private LocalDateTime createTime; private LocalDateTime createTime;
/**
* 笔记发布者 ID
*/
private Long noteCreatorId;
} }

View File

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

View File

@@ -28,4 +28,16 @@
VALUES (#{userId}, #{count}) VALUES (#{userId}, #{count})
ON DUPLICATE KEY UPDATE following_total = following_total + (#{count}); ON DUPLICATE KEY UPDATE following_total = following_total + (#{count});
</insert> </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> </mapper>

View File

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

View File

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

View File

@@ -571,7 +571,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
public Response<?> likeNote(LikeNoteReqVO likeNoteReqVO) { public Response<?> likeNote(LikeNoteReqVO likeNoteReqVO) {
Long noteId = likeNoteReqVO.getId(); Long noteId = likeNoteReqVO.getId();
// 1. 校验被点赞的笔记是否存在 // 1. 校验被点赞的笔记是否存在
checkNoteIsExist(noteId); Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
// 2. 判断目标笔记,是否已经点赞过 // 2. 判断目标笔记,是否已经点赞过
Long userId = LoginUserContextHolder.getUserId(); Long userId = LoginUserContextHolder.getUserId();
@@ -593,7 +593,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
switch (noteLikeLuaResultEnum) { switch (noteLikeLuaResultEnum) {
// Redis 中布隆过滤器不存在 // Redis 中布隆过滤器不存在
case NOT_EXIST -> { case NOT_EXIST -> {
// TODO: 从数据库中校验笔记是否被点赞,并异步初始化布隆过滤器,设置过期时间 //从数据库中校验笔记是否被点赞,并异步初始化布隆过滤器,设置过期时间
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class) long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
.eq(NoteLikeDO::getNoteId, noteId) .eq(NoteLikeDO::getNoteId, noteId)
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode())); .eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
@@ -689,6 +689,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
.noteId(noteId) .noteId(noteId)
.type(LikeStatusEnum.LIKE.getCode()) // 点赞 .type(LikeStatusEnum.LIKE.getCode()) // 点赞
.createTime(now) .createTime(now)
.noteCreatorId(creatorId)
.build(); .build();
// 构建消息将DTO转换为JSON字符串设置到消息体中 // 构建消息将DTO转换为JSON字符串设置到消息体中
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build(); Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
@@ -717,7 +718,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
Long noteId = unlikeNoteReqVO.getId(); Long noteId = unlikeNoteReqVO.getId();
// 1. 校验笔记是否真实存在 // 1. 校验笔记是否真实存在
checkNoteIsExist(noteId); Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
// 2. 校验笔记是否被点赞过 // 2. 校验笔记是否被点赞过
// 当前登录用户ID // 当前登录用户ID
@@ -774,6 +775,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
.noteId(noteId) .noteId(noteId)
.type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode()) // 取消点赞笔记 .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode()) // 取消点赞笔记
.createTime(LocalDateTime.now()) .createTime(LocalDateTime.now())
.noteCreatorId(creatorId)
.build(); .build();
// 构建消息将DTO转换为JSON字符串设置到消息体中 // 构建消息将DTO转换为JSON字符串设置到消息体中
@@ -806,7 +808,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
Long noteId = collectNoteReqVO.getId(); Long noteId = collectNoteReqVO.getId();
// 1. 校验被收藏的笔记是否存在 // 1. 校验被收藏的笔记是否存在
checkNoteIsExist(noteId); Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
// 2. 判断目标笔记,是否已经收藏过 // 2. 判断目标笔记,是否已经收藏过
// 当前登录用户ID // 当前登录用户ID
@@ -828,8 +830,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result); NoteCollectLuaResultEnum noteCollectLuaResultEnum = NoteCollectLuaResultEnum.valueOf(result);
log.info("==> 【笔记收藏】Lua 脚本返回结果: {}", noteCollectLuaResultEnum); log.info("==> 【笔记收藏】Lua 脚本返回结果: {}", noteCollectLuaResultEnum);
assert noteCollectLuaResultEnum != null; switch (Objects.requireNonNull(noteCollectLuaResultEnum)) {
switch (noteCollectLuaResultEnum) {
// 布隆过滤器不存在 // 布隆过滤器不存在
case NOT_EXIST -> { case NOT_EXIST -> {
// 从数据库中校验笔记是否被收藏,并异步初始化布隆过滤器,设置过期时间 // 从数据库中校验笔记是否被收藏,并异步初始化布隆过滤器,设置过期时间
@@ -932,6 +933,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
.noteId(noteId) .noteId(noteId)
.type(CollectUnCollectNoteTypeEnum.COLLECT.getCode()) // 收藏笔记 .type(CollectUnCollectNoteTypeEnum.COLLECT.getCode()) // 收藏笔记
.createTime(now) .createTime(now)
.noteCreatorId(creatorId)
.build(); .build();
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中 // 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
@@ -965,7 +967,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
Long noteId = unCollectNoteReqVO.getId(); Long noteId = unCollectNoteReqVO.getId();
// 1. 校验笔记是否真实存在 // 1. 校验笔记是否真实存在
checkNoteIsExist(noteId); Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
// 2. 校验笔记是否被收藏过 // 2. 校验笔记是否被收藏过
// 当前登录用户ID // 当前登录用户ID
@@ -1021,6 +1023,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
.noteId(noteId) .noteId(noteId)
.type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode()) // 取消收藏笔记 .type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode()) // 取消收藏笔记
.createTime(LocalDateTime.now()) .createTime(LocalDateTime.now())
.noteCreatorId(creatorId)
.build(); .build();
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中 // 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
@@ -1238,17 +1241,17 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
} }
/** /**
* 校验笔记是否存在 * 校验笔记是否存在,若存在,则获取笔记的发布者 ID
* *
* @param noteId 笔记 ID * @param noteId 笔记 ID
*/ */
private void checkNoteIsExist(Long noteId) { private Long checkNoteIsExistAndGetCreatorId(Long noteId) {
// 先从本地缓存中检验 // 先从本地缓存中检验
String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId); String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
// 解析 JSON 为 FindNoteDetailRspVO // 解析 JSON 为 FindNoteDetailRspVO
FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class); FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
// 若缓存不存在 // 若本地缓存不存在
if (Objects.isNull(findNoteDetailRspVO)) { if (Objects.isNull(findNoteDetailRspVO)) {
// 从 Redis 中获取 // 从 Redis 中获取
String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId); String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
@@ -1260,10 +1263,15 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
// 若 Redis 中不存在,则从数据库中获取 // 若 Redis 中不存在,则从数据库中获取
if (Objects.isNull(findNoteDetailRspVO)) { if (Objects.isNull(findNoteDetailRspVO)) {
boolean isExist = this.exists(new LambdaQueryWrapper<>(NoteDO.class) // 查询笔记的发布者用户 ID
NoteDO noteDO = this.getOne(new LambdaQueryWrapper<>(NoteDO.class)
.select(NoteDO::getCreatorId)
.eq(NoteDO::getId, noteId) .eq(NoteDO::getId, noteId)
.eq(NoteDO::getStatus, NoteStatusEnum.NORMAL.getCode())); .eq(NoteDO::getStatus, NoteStatusEnum.NORMAL.getCode()));
if (!isExist) { // 笔记发布者用户 ID
Long creatorId = noteDO.getCreatorId();
// 若数据库中也不存在,提示用户
if (Objects.isNull(creatorId)) {
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND); throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
} }
// 缓存 // 缓存
@@ -1271,8 +1279,11 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
FindNoteDetailReqVO findNoteDetailReqVO = FindNoteDetailReqVO.builder().id(noteId).build(); FindNoteDetailReqVO findNoteDetailReqVO = FindNoteDetailReqVO.builder().id(noteId).build();
findNoteDetail(findNoteDetailReqVO); findNoteDetail(findNoteDetailReqVO);
}); });
return creatorId;
} }
} }
return findNoteDetailRspVO.getCreatorId();
} }
/** /**

View File

@@ -4,10 +4,12 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map; import java.util.Map;
public class JsonUtils { public class JsonUtils {
@@ -74,4 +76,23 @@ public class JsonUtils {
// 将 JSON 字符串转换为 Map // 将 JSON 字符串转换为 Map
return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass)); return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass));
} }
/**
* 将 JSON 字符串解析为指定类型的 List 对象
*
* @param jsonStr JSON 字符串
* @param clazz 目标对象类型
* @param <T> 目标对象类型
* @return List 对象
* @throws Exception 抛出异常
*/
public static <T> List<T> parseList(String jsonStr, Class<T> clazz) throws Exception {
// 使用 TypeReference 指定 List<T> 的泛型类型
return OBJECT_MAPPER.readValue(jsonStr, new TypeReference<List<T>>() {
@Override
public CollectionType getType() {
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz);
}
});
}
} }