Compare commits
3 Commits
bb44cd3d23
...
54c34706fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 54c34706fb | |||
| cfcd12be0d | |||
| 90bd9a5a5d |
@@ -22,4 +22,14 @@ public interface MQConstants {
|
|||||||
*/
|
*/
|
||||||
String TOPIC_COUNT_FOLLOWING_2_DB = "CountFollowing2DBTopic";
|
String TOPIC_COUNT_FOLLOWING_2_DB = "CountFollowing2DBTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记点赞数
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_LIKE = "CountNoteLikeTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记点赞数落库
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_LIKE_2_DB = "CountNoteLike2DBTTopic";
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,26 @@ public class RedisKeyConstants {
|
|||||||
* Hash Field: 粉丝总数
|
* Hash Field: 粉丝总数
|
||||||
*/
|
*/
|
||||||
public static final String FIELD_FANS_TOTAL = "fansTotal";
|
public static final String FIELD_FANS_TOTAL = "fansTotal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash Field: 关注总数
|
* Hash Field: 关注总数
|
||||||
*/
|
*/
|
||||||
public static final String FIELD_FOLLOWING_TOTAL = "followingTotal";
|
public static final String FIELD_FOLLOWING_TOTAL = "followingTotal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户维度计数 Key 前缀
|
* 用户维度计数 Key 前缀
|
||||||
*/
|
*/
|
||||||
private static final String COUNT_USER_KEY_PREFIX = "count:user:";
|
private static final String COUNT_USER_KEY_PREFIX = "count:user:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Field: 笔记点赞总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_LIKE_TOTAL = "likeTotal";
|
||||||
|
/**
|
||||||
|
* 笔记维度计数 Key 前缀
|
||||||
|
*/
|
||||||
|
private static final String COUNT_NOTE_KEY_PREFIX = "count:note:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建用户维度计数 Key
|
* 构建用户维度计数 Key
|
||||||
*
|
*
|
||||||
@@ -25,5 +36,15 @@ public class RedisKeyConstants {
|
|||||||
return COUNT_USER_KEY_PREFIX + userId;
|
return COUNT_USER_KEY_PREFIX + userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建笔记维度计数 Key
|
||||||
|
*
|
||||||
|
* @param noteId 笔记ID
|
||||||
|
* @return 笔记维度计数 Key
|
||||||
|
*/
|
||||||
|
public static String buildCountNoteKey(Long noteId) {
|
||||||
|
return COUNT_NOTE_KEY_PREFIX + noteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.consumer;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
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 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.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@SuppressWarnings({"UnstableApiUsage"})
|
||||||
|
@RocketMQMessageListener(
|
||||||
|
consumerGroup = "han_note_" + MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB,
|
||||||
|
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB
|
||||||
|
)
|
||||||
|
public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
|
||||||
|
|
||||||
|
// 每秒创建 5000 个令牌
|
||||||
|
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
||||||
|
@Resource
|
||||||
|
private NoteCountDOMapper noteCountDOMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String body) {
|
||||||
|
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
|
||||||
|
rateLimiter.acquire();
|
||||||
|
|
||||||
|
log.info("## 消费到了 MQ 【计数: 笔记点赞数入库】, {}...", body);
|
||||||
|
|
||||||
|
Map<Long, Integer> countMap = null;
|
||||||
|
try {
|
||||||
|
countMap = JsonUtils.parseMap(body, Long.class, Integer.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("## 解析 JSON 字符串异常", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollUtil.isNotEmpty(countMap)) {
|
||||||
|
// 判断数据库中 t_note_count 表,若笔记计数记录不存在,则插入;若记录已存在,则直接更新
|
||||||
|
countMap.forEach((k, v) -> noteCountDOMapper.insertOrUpdateLikeTotalByNoteId(v, k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.consumer;
|
||||||
|
|
||||||
|
import com.github.phantomthief.collection.BufferTrigger;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
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.CountLikeUnlikeNoteMqDTO;
|
||||||
|
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.data.redis.core.RedisTemplate;
|
||||||
|
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.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RocketMQMessageListener(
|
||||||
|
consumerGroup = "han_note_" + MQConstants.TOPIC_COUNT_NOTE_LIKE,
|
||||||
|
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE
|
||||||
|
)
|
||||||
|
public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private RocketMQTemplate rocketMQTemplate;
|
||||||
|
|
||||||
|
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
|
||||||
|
.bufferSize(50000) // 缓存队列的最大容量
|
||||||
|
.batchSize(1000) // 一批次最多聚合 1000 条
|
||||||
|
.linger(Duration.ofSeconds(1)) // 多久聚合一次
|
||||||
|
.setConsumerEx(this::consumeMessage) // 设置消费者方法
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String body) {
|
||||||
|
// 往 bufferTrigger 中添加元素
|
||||||
|
bufferTrigger.enqueue(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeMessage(List<String> bodies) {
|
||||||
|
log.info("==> 【笔记点赞数】聚合消息, size: {}", bodies.size());
|
||||||
|
log.info("==> 【笔记点赞数】聚合消息, {}", JsonUtils.toJsonString(bodies));
|
||||||
|
List<CountLikeUnlikeNoteMqDTO> countLikeUnlikeNoteMqDTOS = bodies.stream()
|
||||||
|
.map(body -> JsonUtils.parseObject(body, CountLikeUnlikeNoteMqDTO.class)).toList();
|
||||||
|
// 按笔记ID分组
|
||||||
|
Map<Long, List<CountLikeUnlikeNoteMqDTO>> groupMap = countLikeUnlikeNoteMqDTOS.stream()
|
||||||
|
.collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId));
|
||||||
|
|
||||||
|
// 按组汇总统计处最终计数
|
||||||
|
// key为笔记ID,value为最终操作计数
|
||||||
|
Map<Long, Integer> countMap = Maps.newHashMap();
|
||||||
|
for (Map.Entry<Long, List<CountLikeUnlikeNoteMqDTO>> entry : groupMap.entrySet()) {
|
||||||
|
List<CountLikeUnlikeNoteMqDTO> list = entry.getValue();
|
||||||
|
// 最终计数默认为0
|
||||||
|
int finalCount = 0;
|
||||||
|
for (CountLikeUnlikeNoteMqDTO countLikeUnlikeNoteMqDTO : list) {
|
||||||
|
Integer type = countLikeUnlikeNoteMqDTO.getType();
|
||||||
|
LikeUnlikeNoteTypeEnum likeUnlikeNoteTypeEnum = LikeUnlikeNoteTypeEnum.valueOf(type);
|
||||||
|
if (likeUnlikeNoteTypeEnum == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (likeUnlikeNoteTypeEnum) {
|
||||||
|
case LIKE -> finalCount++;
|
||||||
|
case UNLIKE -> finalCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
countMap.put(entry.getKey(), finalCount);
|
||||||
|
}
|
||||||
|
log.info("## 【笔记点赞数】聚合后的计数数据: {}", JsonUtils.toJsonString(countMap));
|
||||||
|
// 更新Redis
|
||||||
|
countMap.forEach((k, v) -> {
|
||||||
|
// Redis Key
|
||||||
|
String redisKey = RedisKeyConstants.buildCountNoteKey(k);
|
||||||
|
// 判断 Redis 中 Hash 是否存在
|
||||||
|
boolean isExisted = redisTemplate.hasKey(redisKey);
|
||||||
|
|
||||||
|
// 若存在才会更新
|
||||||
|
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
|
||||||
|
if (isExisted) {
|
||||||
|
// 对目标用户 Hash 中的点赞数字段进行计数操作
|
||||||
|
redisTemplate.opsForHash().increment(redisKey, RedisKeyConstants.FIELD_LIKE_TOTAL, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送 MQ, 笔记点赞数据落库
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(countMap))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB, message, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【计数服务:笔记点赞数入库】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【计数服务:笔记点赞数入库】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,17 @@ package com.hanserwei.hannote.count.biz.domain.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO;
|
import com.hanserwei.hannote.count.biz.domain.dataobject.NoteCountDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
|
public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加笔记计数记录或更新笔记点赞数
|
||||||
|
*
|
||||||
|
* @param count 计数
|
||||||
|
* @param noteId 笔记ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int insertOrUpdateLikeTotalByNoteId(@Param("count") Integer count, @Param("noteId") Long noteId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum LikeUnlikeNoteTypeEnum {
|
||||||
|
// 点赞
|
||||||
|
LIKE(1),
|
||||||
|
// 取消点赞
|
||||||
|
UNLIKE(0),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
public static LikeUnlikeNoteTypeEnum valueOf(Integer code) {
|
||||||
|
for (LikeUnlikeNoteTypeEnum likeUnlikeNoteTypeEnum : LikeUnlikeNoteTypeEnum.values()) {
|
||||||
|
if (Objects.equals(code, likeUnlikeNoteTypeEnum.getCode())) {
|
||||||
|
return likeUnlikeNoteTypeEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class CountLikeUnlikeNoteMqDTO {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private Long noteId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0: 取消点赞, 1:点赞
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
}
|
||||||
@@ -14,4 +14,10 @@
|
|||||||
<!--@mbg.generated-->
|
<!--@mbg.generated-->
|
||||||
id, note_id, like_total, collect_total, comment_total
|
id, note_id, like_total, collect_total, comment_total
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
|
<insert id="insertOrUpdateLikeTotalByNoteId" parameterType="map">
|
||||||
|
INSERT INTO t_note_count (note_id, like_total)
|
||||||
|
VALUES (#{noteId}, #{count})
|
||||||
|
ON DUPLICATE KEY UPDATE like_total = like_total + (#{count});
|
||||||
|
</insert>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
package com.hanserwei.hannote.note.biz.comsumer;
|
package com.hanserwei.hannote.note.biz.comsumer;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
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.note.biz.constant.MQConstants;
|
import com.hanserwei.hannote.note.biz.constant.MQConstants;
|
||||||
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
|
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
|
||||||
import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
|
import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
|
||||||
|
import com.hanserwei.hannote.note.biz.enums.LikeUnlikeNoteTypeEnum;
|
||||||
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
||||||
|
import com.hanserwei.hannote.note.biz.service.NoteLikeDOService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.rocketmq.client.producer.SendCallback;
|
||||||
|
import org.apache.rocketmq.client.producer.SendResult;
|
||||||
import org.apache.rocketmq.common.message.Message;
|
import org.apache.rocketmq.common.message.Message;
|
||||||
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
import org.apache.rocketmq.spring.annotation.ConsumeMode;
|
||||||
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.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings({"UnstableApiUsage"})
|
||||||
@Component
|
@Component
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
consumerGroup = "han_note_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
||||||
@@ -31,6 +38,10 @@ public class LikeUnlikeNoteConsumer implements RocketMQListener<Message> {
|
|||||||
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
||||||
@Resource
|
@Resource
|
||||||
private NoteLikeDOMapper noteLikeDOMapper;
|
private NoteLikeDOMapper noteLikeDOMapper;
|
||||||
|
@Resource
|
||||||
|
private NoteLikeDOService noteLikeDOService;
|
||||||
|
@Resource
|
||||||
|
private RocketMQTemplate rocketMQTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(Message message) {
|
public void onMessage(Message message) {
|
||||||
@@ -60,7 +71,54 @@ public class LikeUnlikeNoteConsumer implements RocketMQListener<Message> {
|
|||||||
* @param bodyJsonStr 消息体
|
* @param bodyJsonStr 消息体
|
||||||
*/
|
*/
|
||||||
private void handleUnlikeNoteTagMessage(String bodyJsonStr) {
|
private void handleUnlikeNoteTagMessage(String bodyJsonStr) {
|
||||||
|
// 消息体 JSON 字符串转 DTO
|
||||||
|
LikeUnlikeNoteMqDTO unlikeNoteMqDTO = JsonUtils.parseObject(bodyJsonStr, LikeUnlikeNoteMqDTO.class);
|
||||||
|
if (Objects.isNull(unlikeNoteMqDTO)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 用户ID
|
||||||
|
Long userId = unlikeNoteMqDTO.getUserId();
|
||||||
|
// 点赞的笔记ID
|
||||||
|
Long noteId = unlikeNoteMqDTO.getNoteId();
|
||||||
|
// 操作类型
|
||||||
|
Integer type = unlikeNoteMqDTO.getType();
|
||||||
|
// 取消点赞时间
|
||||||
|
LocalDateTime createTime = unlikeNoteMqDTO.getCreateTime();
|
||||||
|
|
||||||
|
// 设置要更新的字段值
|
||||||
|
NoteLikeDO updateEntity = NoteLikeDO.builder()
|
||||||
|
.createTime(createTime) // 更新时间
|
||||||
|
.status(type) // 设置新的状态值 (例如 0 表示取消点赞)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 设置更新条件:where user_id = [userId] and note_id = [noteId] and status = 1
|
||||||
|
LambdaQueryWrapper<NoteLikeDO> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(NoteLikeDO::getUserId, userId)
|
||||||
|
.eq(NoteLikeDO::getNoteId, noteId)
|
||||||
|
.eq(NoteLikeDO::getStatus, LikeUnlikeNoteTypeEnum.LIKE.getCode()); // 确保只更新当前为“已点赞”的记录
|
||||||
|
|
||||||
|
// 执行更新
|
||||||
|
boolean update = noteLikeDOService.update(updateEntity, wrapper);
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新数据库成功后,发送计数 MQ
|
||||||
|
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(bodyJsonStr)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_LIKE, message, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【计数: 笔记取消点赞】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【计数: 笔记取消点赞】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,7 +152,27 @@ public class LikeUnlikeNoteConsumer implements RocketMQListener<Message> {
|
|||||||
// 添加或更新笔记点赞记录
|
// 添加或更新笔记点赞记录
|
||||||
boolean count = noteLikeDOMapper.insertOrUpdate(noteLikeDO);
|
boolean count = noteLikeDOMapper.insertOrUpdate(noteLikeDO);
|
||||||
|
|
||||||
// TODO: 发送计数 MQ
|
if (!count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送计数 MQ
|
||||||
|
// 更新数据库成功后,发送计数 MQ
|
||||||
|
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(bodyJsonStr)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_LIKE, message, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【计数: 笔记点赞】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【计数: 笔记点赞】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ public interface MQConstants {
|
|||||||
*/
|
*/
|
||||||
String TOPIC_LIKE_OR_UNLIKE = "LikeUnlikeTopic";
|
String TOPIC_LIKE_OR_UNLIKE = "LikeUnlikeTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记点赞数
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_LIKE = "CountNoteLikeTopic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点赞标签
|
* 点赞标签
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,4 +62,10 @@ public class NoteController {
|
|||||||
return noteService.likeNote(likeNoteReqVO);
|
return noteService.likeNote(likeNoteReqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/unlike")
|
||||||
|
@ApiOperationLog(description = "取消点赞笔记")
|
||||||
|
public Response<?> unlikeNote(@Validated @RequestBody UnlikeNoteReqVO unlikeNoteReqVO) {
|
||||||
|
return noteService.unlikeNote(unlikeNoteReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.hanserwei.hannote.note.biz.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum NoteUnlikeLuaResultEnum {
|
||||||
|
// 布隆过滤器不存在
|
||||||
|
NOT_EXIST(-1L),
|
||||||
|
// 笔记已点赞
|
||||||
|
NOTE_LIKED(1L),
|
||||||
|
// 笔记未点赞
|
||||||
|
NOTE_NOT_LIKED(0L),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Long code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型 code 获取对应的枚举
|
||||||
|
*
|
||||||
|
* @param code 类型 code
|
||||||
|
* @return 枚举
|
||||||
|
*/
|
||||||
|
public static NoteUnlikeLuaResultEnum valueOf(Long code) {
|
||||||
|
for (NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum : NoteUnlikeLuaResultEnum.values()) {
|
||||||
|
if (Objects.equals(code, noteUnlikeLuaResultEnum.getCode())) {
|
||||||
|
return noteUnlikeLuaResultEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
|||||||
NOTE_CANT_VISIBLE_ONLY_ME("NOTE-20006", "此笔记无法修改为仅自己可见"),
|
NOTE_CANT_VISIBLE_ONLY_ME("NOTE-20006", "此笔记无法修改为仅自己可见"),
|
||||||
NOTE_CANT_OPERATE("NOTE-20007", "您无法操作该笔记"),
|
NOTE_CANT_OPERATE("NOTE-20007", "您无法操作该笔记"),
|
||||||
NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"),
|
NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"),
|
||||||
|
NOTE_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"),
|
||||||
;
|
;
|
||||||
|
|
||||||
// 异常码
|
// 异常码
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hanserwei.hannote.note.biz.model.vo;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class UnlikeNoteReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "笔记 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -57,4 +57,12 @@ public interface NoteService extends IService<NoteDO> {
|
|||||||
*/
|
*/
|
||||||
Response<?> likeNote(LikeNoteReqVO likeNoteReqVO);
|
Response<?> likeNote(LikeNoteReqVO likeNoteReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消点赞笔记
|
||||||
|
*
|
||||||
|
* @param unlikeNoteReqVO 取消点赞笔记请求
|
||||||
|
* @return 取消点赞笔记结果
|
||||||
|
*/
|
||||||
|
Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -705,6 +705,95 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
|
||||||
|
// 笔记ID
|
||||||
|
Long noteId = unlikeNoteReqVO.getId();
|
||||||
|
|
||||||
|
// 1. 校验笔记是否真实存在
|
||||||
|
checkNoteIsExist(noteId);
|
||||||
|
|
||||||
|
// 2. 校验笔记是否被点赞过
|
||||||
|
// 当前登录用户ID
|
||||||
|
Long userId = LoginUserContextHolder.getUserId();
|
||||||
|
|
||||||
|
// 布隆过滤器Key
|
||||||
|
String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
|
||||||
|
|
||||||
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||||
|
// Lua 脚本路径
|
||||||
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
|
||||||
|
script.setResultType(Long.class);
|
||||||
|
|
||||||
|
// 执行 Lua 脚本,拿到返回结果
|
||||||
|
Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
|
||||||
|
|
||||||
|
NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);
|
||||||
|
log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum);
|
||||||
|
assert noteUnlikeLuaResultEnum != null;
|
||||||
|
switch (noteUnlikeLuaResultEnum) {
|
||||||
|
// 布隆过滤器不存在
|
||||||
|
case NOT_EXIST -> {
|
||||||
|
//笔记不存在
|
||||||
|
//异步初始化布隆过滤器
|
||||||
|
threadPoolTaskExecutor.submit(() -> {
|
||||||
|
// 保底1天+随机秒数
|
||||||
|
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||||
|
batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
|
||||||
|
// 从数据库中校验笔记是否被点赞
|
||||||
|
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||||
|
.eq(NoteLikeDO::getUserId, userId)
|
||||||
|
.eq(NoteLikeDO::getNoteId, noteId)
|
||||||
|
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
|
||||||
|
if (count == 0) {
|
||||||
|
log.info("==> 【笔记取消点赞】用户未点赞该笔记");
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 布隆过滤器校验目标笔记未被点赞(判断绝对正确)
|
||||||
|
case NOTE_NOT_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 能走到这里,说明布隆过滤器判断已点赞,直接删除 ZSET 中已点赞的笔记 ID
|
||||||
|
// 用户点赞列表ZsetKey
|
||||||
|
String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
|
||||||
|
|
||||||
|
redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);
|
||||||
|
|
||||||
|
//4. 发送 MQ, 数据更新落库
|
||||||
|
// 构建MQ消息体
|
||||||
|
LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.noteId(noteId)
|
||||||
|
.type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode()) // 取消点赞笔记
|
||||||
|
.createTime(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建消息,将DTO转换为JSON字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build();
|
||||||
|
|
||||||
|
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||||
|
String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE;
|
||||||
|
|
||||||
|
String hashKey = String.valueOf(noteId);
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息,提升接口响应速度
|
||||||
|
rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【笔记取消点赞】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【笔记取消点赞】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步初始化用户点赞笔记 ZSet
|
* 异步初始化用户点赞笔记 ZSet
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
local key = KEYS[1] -- 操作的 Redis Key
|
||||||
|
local noteId = ARGV[1] -- 笔记ID
|
||||||
|
|
||||||
|
-- 使用 EXISTS 命令检查布隆过滤器是否存在
|
||||||
|
local exists = redis.call('EXISTS', key)
|
||||||
|
if exists == 0 then
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 校验该篇笔记是否被点赞过(1 表示已经点赞,0 表示未点赞)
|
||||||
|
return redis.call('BF.EXISTS', key, noteId)
|
||||||
@@ -191,5 +191,14 @@ Content-Type: application/json
|
|||||||
Authorization: Bearer {{token}}
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": {{noteId}}
|
"id": 1977249693272375330
|
||||||
|
}
|
||||||
|
|
||||||
|
### 笔记取消点赞入口
|
||||||
|
POST http://localhost:8000/note/note/unlike
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1977249693272375330
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user