Compare commits
4 Commits
61cfbd6b81
...
7fc24e1e2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fc24e1e2a | |||
| 7b1df60c05 | |||
| 564eefa7bc | |||
| c036fadbff |
@@ -32,4 +32,29 @@ public interface MQConstants {
|
|||||||
*/
|
*/
|
||||||
String TOPIC_COUNT_NOTE_LIKE_2_DB = "CountNoteLike2DBTTopic";
|
String TOPIC_COUNT_NOTE_LIKE_2_DB = "CountNoteLike2DBTTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记收藏数
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_COLLECT = "CountNoteCollectTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记收藏数落库
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_COLLECT_2_DB = "CountNoteCollect2DBTTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 笔记操作(发布、删除)
|
||||||
|
*/
|
||||||
|
String TOPIC_NOTE_OPERATE = "NoteOperateTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag 标签:笔记发布
|
||||||
|
*/
|
||||||
|
String TAG_NOTE_PUBLISH = "publishNote";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag 标签:笔记删除
|
||||||
|
*/
|
||||||
|
String TAG_NOTE_DELETE = "deleteNote";
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,11 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String FIELD_FOLLOWING_TOTAL = "followingTotal";
|
public static final String FIELD_FOLLOWING_TOTAL = "followingTotal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Field: 笔记发布总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_NOTE_TOTAL = "noteTotal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户维度计数 Key 前缀
|
* 用户维度计数 Key 前缀
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +31,11 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
private static final String COUNT_NOTE_KEY_PREFIX = "count:note:";
|
private static final String COUNT_NOTE_KEY_PREFIX = "count:note:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash Field: 笔记收藏总数
|
||||||
|
*/
|
||||||
|
public static final String FIELD_COLLECT_TOTAL = "collectTotal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建用户维度计数 Key
|
* 构建用户维度计数 Key
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
@SuppressWarnings("ALL")
|
@SuppressWarnings("ALL")
|
||||||
@Component
|
@Component
|
||||||
@RocketMQMessageListener(consumerGroup = "han_note_" + MQConstants.TOPIC_COUNT_FANS_2_DB, // Group 组
|
@RocketMQMessageListener(consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_FANS_2_DB, // Group 组
|
||||||
topic = MQConstants.TOPIC_COUNT_FANS_2_DB // 主题 Topic
|
topic = MQConstants.TOPIC_COUNT_FANS_2_DB // 主题 Topic
|
||||||
)
|
)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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 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.List;
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RocketMQMessageListener(
|
||||||
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_NOTE_COLLECT_2_DB,
|
||||||
|
topic = MQConstants.TOPIC_COUNT_NOTE_COLLECT_2_DB
|
||||||
|
)
|
||||||
|
public class CountNoteCollect2DBConsumer implements RocketMQListener<String> {
|
||||||
|
|
||||||
|
// 每秒创建 5000 个令牌
|
||||||
|
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) {
|
||||||
|
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
|
||||||
|
rateLimiter.acquire();
|
||||||
|
|
||||||
|
log.info("## 消费到了 MQ 【计数: 笔记收藏数入库】, {}...", body);
|
||||||
|
|
||||||
|
List<AggregationCountCollectedUncollectedNoteMqDTO> countList = null;
|
||||||
|
try {
|
||||||
|
countList = JsonUtils.parseList(body, AggregationCountCollectedUncollectedNoteMqDTO.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("## 解析 JSON 字符串异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.consumer;
|
||||||
|
|
||||||
|
import com.github.phantomthief.collection.BufferTrigger;
|
||||||
|
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;
|
||||||
|
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.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RocketMQMessageListener(
|
||||||
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_NOTE_COLLECT,
|
||||||
|
topic = MQConstants.TOPIC_COUNT_NOTE_COLLECT
|
||||||
|
)
|
||||||
|
public class CountNoteCollectConsumer 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<String> -> List<CountCollectUnCollectNoteMqDTO>
|
||||||
|
List<CountCollectUnCollectNoteMqDTO> countCollectUnCollectNoteMqDTOS = bodies.stream()
|
||||||
|
.map(body -> JsonUtils.parseObject(body, CountCollectUnCollectNoteMqDTO.class))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 按笔记ID分组
|
||||||
|
Map<Long, List<CountCollectUnCollectNoteMqDTO>> groupMap = countCollectUnCollectNoteMqDTOS.stream()
|
||||||
|
.collect(Collectors.groupingBy(CountCollectUnCollectNoteMqDTO::getNoteId));
|
||||||
|
// 按组汇总数据,统计出最终的计数
|
||||||
|
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)) {
|
||||||
|
case COLLECT -> finalCount++;
|
||||||
|
case UN_COLLECT -> finalCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 将分组后统计出的最终计数,存入 countList 中
|
||||||
|
countList.add(AggregationCountCollectedUncollectedNoteMqDTO.builder()
|
||||||
|
.noteId(noteId)
|
||||||
|
.creatorId(creatorId)
|
||||||
|
.count(finalCount)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
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 isCountNoteExisted = redisTemplate.hasKey(countNoteRedisKey);
|
||||||
|
// 若存在才会更新
|
||||||
|
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
|
||||||
|
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(countList))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_COLLECT_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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,22 @@ 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
|
||||||
@SuppressWarnings({"UnstableApiUsage"})
|
@SuppressWarnings({"UnstableApiUsage"})
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_" + MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB,
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB,
|
||||||
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB
|
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE_2_DB
|
||||||
)
|
)
|
||||||
public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
|
public class CountNoteLike2DBConsumer implements RocketMQListener<String> {
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -27,7 +28,7 @@ import java.util.stream.Collectors;
|
|||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_" + MQConstants.TOPIC_COUNT_NOTE_LIKE,
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COUNT_NOTE_LIKE,
|
||||||
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE
|
topic = MQConstants.TOPIC_COUNT_NOTE_LIKE
|
||||||
)
|
)
|
||||||
public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
||||||
@@ -60,13 +61,18 @@ public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
|||||||
.collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId));
|
.collect(Collectors.groupingBy(CountLikeUnlikeNoteMqDTO::getNoteId));
|
||||||
|
|
||||||
// 按组汇总统计处最终计数
|
// 按组汇总统计处最终计数
|
||||||
// key为笔记ID,value为最终操作计数
|
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 消息
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.hanserwei.hannote.count.biz.consumer;
|
||||||
|
|
||||||
|
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.domain.mapper.UserCountDOMapper;
|
||||||
|
import com.hanserwei.hannote.count.biz.model.dto.NoteOperateMqDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.rocketmq.common.message.Message;
|
||||||
|
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||||
|
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RocketMQMessageListener(
|
||||||
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_NOTE_OPERATE,
|
||||||
|
topic = MQConstants.TOPIC_NOTE_OPERATE
|
||||||
|
)
|
||||||
|
public class CountNotePublishConsumer implements RocketMQListener<Message> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private UserCountDOMapper userCountDOMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(Message message) {
|
||||||
|
// 消息体
|
||||||
|
String bodyJsonStr = new String(message.getBody());
|
||||||
|
// 标签
|
||||||
|
String tags = message.getTags();
|
||||||
|
|
||||||
|
log.info("==> CountNotePublishConsumer 消费了消息 {}, tags: {}", bodyJsonStr, tags);
|
||||||
|
|
||||||
|
// 根据 MQ 标签,判断笔记操作类型
|
||||||
|
if (Objects.equals(tags, MQConstants.TAG_NOTE_PUBLISH)) { // 笔记发布
|
||||||
|
handleTagMessage(bodyJsonStr, 1);
|
||||||
|
} else if (Objects.equals(tags, MQConstants.TAG_NOTE_DELETE)) { // 笔记删除
|
||||||
|
handleTagMessage(bodyJsonStr, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理笔记发布和笔记删除的 MQ 消息
|
||||||
|
*
|
||||||
|
* @param bodyJsonStr 笔记发布或删除的 MQ 消息体
|
||||||
|
* @param count 笔记发布或删除的计数
|
||||||
|
*/
|
||||||
|
private void handleTagMessage(String bodyJsonStr, long count) {
|
||||||
|
// 消息体 JSON 字符串转 DTO
|
||||||
|
NoteOperateMqDTO noteOperateMqDTO = JsonUtils.parseObject(bodyJsonStr, NoteOperateMqDTO.class);
|
||||||
|
|
||||||
|
if (Objects.isNull(noteOperateMqDTO)) return;
|
||||||
|
|
||||||
|
// 笔记发布者 ID
|
||||||
|
Long creatorId = noteOperateMqDTO.getCreatorId();
|
||||||
|
|
||||||
|
// 更新 Redis 中用户维度的计数 Hash
|
||||||
|
String countUserRedisKey = RedisKeyConstants.buildCountUserKey(creatorId);
|
||||||
|
// 判断 Redis 中 Hash 是否存在
|
||||||
|
boolean isCountUserExisted = redisTemplate.hasKey(countUserRedisKey);
|
||||||
|
|
||||||
|
// 若存在才会更新
|
||||||
|
// (因为缓存设有过期时间,考虑到过期后,缓存会被删除,这里需要判断一下,存在才会去更新,而初始化工作放在查询计数来做)
|
||||||
|
if (isCountUserExisted) {
|
||||||
|
// 对目标用户 Hash 中的笔记发布总数,进行加减操作
|
||||||
|
redisTemplate.opsForHash().increment(countUserRedisKey, RedisKeyConstants.FIELD_NOTE_TOTAL, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 t_user_count 表
|
||||||
|
userCountDOMapper.insertOrUpdateNoteTotalByUserId(count, creatorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,4 +16,13 @@ public interface NoteCountDOMapper extends BaseMapper<NoteCountDO> {
|
|||||||
* @return 影响行数
|
* @return 影响行数
|
||||||
*/
|
*/
|
||||||
int insertOrUpdateLikeTotalByNoteId(@Param("count") Integer count, @Param("noteId") Long noteId);
|
int insertOrUpdateLikeTotalByNoteId(@Param("count") Integer count, @Param("noteId") Long noteId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记录或更新笔记收藏数
|
||||||
|
*
|
||||||
|
* @param count 收藏数
|
||||||
|
* @param noteId 笔记ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int insertOrUpdateCollectTotalByNoteId(@Param("count") Integer count, @Param("noteId") Long noteId);
|
||||||
}
|
}
|
||||||
@@ -25,4 +25,31 @@ 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记录或更新笔记发布数
|
||||||
|
*
|
||||||
|
* @param count 笔记发布数
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int insertOrUpdateNoteTotalByUserId(@Param("count") Long count, @Param("userId") Long userId);
|
||||||
}
|
}
|
||||||
@@ -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 CollectUnCollectNoteTypeEnum {
|
||||||
|
// 收藏
|
||||||
|
COLLECT(1),
|
||||||
|
// 取消收藏
|
||||||
|
UN_COLLECT(0),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
public static CollectUnCollectNoteTypeEnum valueOf(Integer code) {
|
||||||
|
for (CollectUnCollectNoteTypeEnum collectUnCollectNoteTypeEnum : CollectUnCollectNoteTypeEnum.values()) {
|
||||||
|
if (Objects.equals(code, collectUnCollectNoteTypeEnum.getCode())) {
|
||||||
|
return collectUnCollectNoteTypeEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 CountCollectUnCollectNoteMqDTO {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private Long noteId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0: 取消收藏, 1:收藏
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long noteCreatorId;
|
||||||
|
}
|
||||||
@@ -23,4 +23,9 @@ public class CountLikeUnlikeNoteMqDTO {
|
|||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long noteCreatorId;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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 NoteOperateMqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记 ID
|
||||||
|
*/
|
||||||
|
private Long noteId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作类型: 0 - 笔记删除; 1:笔记发布;
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,4 +20,10 @@
|
|||||||
VALUES (#{noteId}, #{count})
|
VALUES (#{noteId}, #{count})
|
||||||
ON DUPLICATE KEY UPDATE like_total = like_total + (#{count});
|
ON DUPLICATE KEY UPDATE like_total = like_total + (#{count});
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<insert id="insertOrUpdateCollectTotalByNoteId" parameterType="map">
|
||||||
|
INSERT INTO t_note_count (note_id, collect_total)
|
||||||
|
VALUES (#{noteId}, #{count})
|
||||||
|
ON DUPLICATE KEY UPDATE collect_total = collect_total + (#{count});
|
||||||
|
</insert>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -28,4 +28,22 @@
|
|||||||
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>
|
||||||
|
|
||||||
|
<insert id="insertOrUpdateNoteTotalByUserId" parameterType="map">
|
||||||
|
INSERT INTO t_user_count (user_id, note_total)
|
||||||
|
VALUES (#{userId}, #{count})
|
||||||
|
ON DUPLICATE KEY UPDATE note_total = note_total + (#{count});
|
||||||
|
</insert>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -8,20 +8,24 @@ import com.hanserwei.hannote.note.biz.domain.mapper.NoteCollectionDOMapper;
|
|||||||
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
||||||
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", "DuplicatedCode"})
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_" + MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
||||||
topic = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
topic = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT,
|
||||||
consumeMode = ConsumeMode.ORDERLY
|
consumeMode = ConsumeMode.ORDERLY
|
||||||
)
|
)
|
||||||
@@ -31,6 +35,8 @@ public class CollectUnCollectNoteConsumer implements RocketMQListener<Message> {
|
|||||||
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
private final RateLimiter rateLimiter = RateLimiter.create(5000);
|
||||||
@Resource
|
@Resource
|
||||||
private NoteCollectionDOMapper noteCollectionDOMapper;
|
private NoteCollectionDOMapper noteCollectionDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RocketMQTemplate rocketMQTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(Message message) {
|
public void onMessage(Message message) {
|
||||||
@@ -60,6 +66,51 @@ public class CollectUnCollectNoteConsumer implements RocketMQListener<Message> {
|
|||||||
* @param bodyJsonStr 消息体
|
* @param bodyJsonStr 消息体
|
||||||
*/
|
*/
|
||||||
private void handleUnCollectNoteTagMessage(String bodyJsonStr) {
|
private void handleUnCollectNoteTagMessage(String bodyJsonStr) {
|
||||||
|
// 消息体 JSON 字符串转 DTO
|
||||||
|
CollectUnCollectNoteMqDTO unCollectNoteMqDTO = JsonUtils.parseObject(bodyJsonStr, CollectUnCollectNoteMqDTO.class);
|
||||||
|
|
||||||
|
if (Objects.isNull(unCollectNoteMqDTO)) return;
|
||||||
|
|
||||||
|
// 用户ID
|
||||||
|
Long userId = unCollectNoteMqDTO.getUserId();
|
||||||
|
// 收藏的笔记ID
|
||||||
|
Long noteId = unCollectNoteMqDTO.getNoteId();
|
||||||
|
// 操作类型
|
||||||
|
Integer type = unCollectNoteMqDTO.getType();
|
||||||
|
// 收藏时间
|
||||||
|
LocalDateTime createTime = unCollectNoteMqDTO.getCreateTime();
|
||||||
|
|
||||||
|
// 构建 DO 对象
|
||||||
|
NoteCollectionDO noteCollectionDO = NoteCollectionDO.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.noteId(noteId)
|
||||||
|
.createTime(createTime)
|
||||||
|
.status(type)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 取消收藏:记录更新
|
||||||
|
int count = noteCollectionDOMapper.update2UnCollectByUserIdAndNoteId(noteCollectionDO);
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据库成功后,发送计数 MQ
|
||||||
|
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(bodyJsonStr)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_COLLECT, 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,6 +145,28 @@ public class CollectUnCollectNoteConsumer implements RocketMQListener<Message> {
|
|||||||
// 添加或更新笔记收藏记录
|
// 添加或更新笔记收藏记录
|
||||||
boolean isSuccess = noteCollectionDOMapper.insertOrUpdate(noteCollectionDO);
|
boolean isSuccess = noteCollectionDOMapper.insertOrUpdate(noteCollectionDO);
|
||||||
|
|
||||||
// TODO: 发送计数 MQ
|
if (!isSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送计数 MQ
|
||||||
|
|
||||||
|
// 更新数据库成功后,发送计数 MQ
|
||||||
|
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(bodyJsonStr)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
// 异步发送 MQ 消息
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_COUNT_NOTE_COLLECT, message, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【计数: 笔记收藏】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【计数: 笔记收藏】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import java.util.Objects;
|
|||||||
@SuppressWarnings({"UnstableApiUsage"})
|
@SuppressWarnings({"UnstableApiUsage"})
|
||||||
@Component
|
@Component
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
consumerGroup = "han_note_group_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
||||||
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
||||||
consumeMode = ConsumeMode.ORDERLY// 顺序消费
|
consumeMode = ConsumeMode.ORDERLY// 顺序消费
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ public interface MQConstants {
|
|||||||
*/
|
*/
|
||||||
String TOPIC_COLLECT_OR_UN_COLLECT = "CollectUnCollectTopic";
|
String TOPIC_COLLECT_OR_UN_COLLECT = "CollectUnCollectTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 计数 - 笔记收藏数
|
||||||
|
*/
|
||||||
|
String TOPIC_COUNT_NOTE_COLLECT = "CountNoteCollectTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 笔记操作(发布、删除)
|
||||||
|
*/
|
||||||
|
String TOPIC_NOTE_OPERATE = "NoteOperateTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag 标签:笔记发布
|
||||||
|
*/
|
||||||
|
String TAG_NOTE_PUBLISH = "publishNote";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag 标签:笔记删除
|
||||||
|
*/
|
||||||
|
String TAG_NOTE_DELETE = "deleteNote";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点赞标签
|
* 点赞标签
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -74,4 +74,10 @@ public class NoteController {
|
|||||||
return noteService.collectNote(collectNoteReqVO);
|
return noteService.collectNote(collectNoteReqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/uncollect")
|
||||||
|
@ApiOperationLog(description = "取消收藏笔记")
|
||||||
|
public Response<?> unCollectNote(@Validated @RequestBody UnCollectNoteReqVO unCollectNoteReqVO) {
|
||||||
|
return noteService.unCollectNote(unCollectNoteReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,12 @@ public interface NoteCollectionDOMapper extends BaseMapper<NoteCollectionDO> {
|
|||||||
* @return 是否成功
|
* @return 是否成功
|
||||||
*/
|
*/
|
||||||
boolean insertOrUpdate(NoteCollectionDO noteCollectionDO);
|
boolean insertOrUpdate(NoteCollectionDO noteCollectionDO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消点赞
|
||||||
|
*
|
||||||
|
* @param noteCollectionDO 笔记收藏记录
|
||||||
|
* @return 影响行数
|
||||||
|
*/
|
||||||
|
int update2UnCollectByUserIdAndNoteId(NoteCollectionDO noteCollectionDO);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hanserwei.hannote.note.biz.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum NoteOperateEnum {
|
||||||
|
// 笔记发布
|
||||||
|
PUBLISH(1),
|
||||||
|
// 笔记删除
|
||||||
|
DELETE(0),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 NoteUnCollectLuaResultEnum {
|
||||||
|
// 布隆过滤器不存在
|
||||||
|
NOT_EXIST(-1L),
|
||||||
|
// 笔记已收藏
|
||||||
|
NOTE_COLLECTED(1L),
|
||||||
|
// 笔记未收藏
|
||||||
|
NOTE_NOT_COLLECTED(0L),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final Long code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型 code 获取对应的枚举
|
||||||
|
*
|
||||||
|
* @param code 类型 code
|
||||||
|
* @return 对应的枚举
|
||||||
|
*/
|
||||||
|
public static NoteUnCollectLuaResultEnum valueOf(Long code) {
|
||||||
|
for (NoteUnCollectLuaResultEnum noteUnCollectLuaResultEnum : NoteUnCollectLuaResultEnum.values()) {
|
||||||
|
if (Objects.equals(code, noteUnCollectLuaResultEnum.getCode())) {
|
||||||
|
return noteUnCollectLuaResultEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
|||||||
NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"),
|
NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"),
|
||||||
NOTE_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"),
|
NOTE_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"),
|
||||||
NOTE_ALREADY_COLLECTED("NOTE-20010", "您已经收藏过该笔记"),
|
NOTE_ALREADY_COLLECTED("NOTE-20010", "您已经收藏过该笔记"),
|
||||||
|
NOTE_NOT_COLLECTED("NOTE-20011", "您未收藏该篇笔记,无法取消收藏"),
|
||||||
;
|
;
|
||||||
|
|
||||||
// 异常码
|
// 异常码
|
||||||
|
|||||||
@@ -23,4 +23,9 @@ public class CollectUnCollectNoteMqDTO {
|
|||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long noteCreatorId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,9 @@ public class LikeUnlikeNoteMqDTO {
|
|||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long noteCreatorId;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.hanserwei.hannote.note.biz.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class NoteOperateMqDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记发布者 ID
|
||||||
|
*/
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记 ID
|
||||||
|
*/
|
||||||
|
private Long noteId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作类型: 0 - 笔记删除; 1:笔记发布;
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 UnCollectNoteReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "笔记 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -73,4 +73,12 @@ public interface NoteService extends IService<NoteDO> {
|
|||||||
*/
|
*/
|
||||||
Response<?> collectNote(CollectNoteReqVO collectNoteReqVO);
|
Response<?> collectNote(CollectNoteReqVO collectNoteReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消收藏笔记
|
||||||
|
*
|
||||||
|
* @param unCollectNoteReqVO 取消收藏笔记请求
|
||||||
|
* @return 取消收藏笔记结果
|
||||||
|
*/
|
||||||
|
Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
|
|||||||
import com.hanserwei.hannote.note.biz.enums.*;
|
import com.hanserwei.hannote.note.biz.enums.*;
|
||||||
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
||||||
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
||||||
|
import com.hanserwei.hannote.note.biz.model.dto.NoteOperateMqDTO;
|
||||||
import com.hanserwei.hannote.note.biz.model.vo.*;
|
import com.hanserwei.hannote.note.biz.model.vo.*;
|
||||||
import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService;
|
import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService;
|
||||||
import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService;
|
import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService;
|
||||||
@@ -40,7 +41,6 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.rocketmq.client.producer.SendCallback;
|
import org.apache.rocketmq.client.producer.SendCallback;
|
||||||
import org.apache.rocketmq.client.producer.SendResult;
|
import org.apache.rocketmq.client.producer.SendResult;
|
||||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
@@ -88,7 +88,7 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
.build();
|
.build();
|
||||||
@Resource
|
@Resource
|
||||||
private NoteLikeDOService noteLikeDOService;
|
private NoteLikeDOService noteLikeDOService;
|
||||||
@Autowired
|
@Resource
|
||||||
private NoteCollectionDOService noteCollectionDOService;
|
private NoteCollectionDOService noteCollectionDOService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -190,6 +190,33 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 发送 MQ
|
||||||
|
// 构建消息体 DTO
|
||||||
|
NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
|
||||||
|
.creatorId(creatorId)
|
||||||
|
.noteId(Long.valueOf(snowflakeId))
|
||||||
|
.type(NoteOperateEnum.PUBLISH.getCode()) // 发布笔记
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||||
|
String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_PUBLISH;
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息,提升接口响应速度
|
||||||
|
rocketMQTemplate.asyncSend(destination, message, 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();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +525,34 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
|
rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
|
||||||
log.info("====> MQ:删除笔记,删除本地缓存发送成功...");
|
log.info("====> MQ:删除笔记,删除本地缓存发送成功...");
|
||||||
|
|
||||||
|
// 发送 MQ
|
||||||
|
// 构建消息体 DTO
|
||||||
|
NoteOperateMqDTO noteOperateMqDTO = NoteOperateMqDTO.builder()
|
||||||
|
.creatorId(selectNoteDO.getCreatorId())
|
||||||
|
.noteId(noteId)
|
||||||
|
.type(NoteOperateEnum.DELETE.getCode()) // 删除笔记
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(noteOperateMqDTO))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||||
|
String destination = MQConstants.TOPIC_NOTE_OPERATE + ":" + MQConstants.TAG_NOTE_DELETE;
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息,提升接口响应速度
|
||||||
|
rocketMQTemplate.asyncSend(destination, message, 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();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +626,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 +648,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 +744,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 +773,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 +830,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 +863,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 +885,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 +988,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 字符串设置到消息体中
|
||||||
@@ -959,6 +1016,96 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO) {
|
||||||
|
// 笔记ID
|
||||||
|
Long noteId = unCollectNoteReqVO.getId();
|
||||||
|
|
||||||
|
// 1. 校验笔记是否真实存在
|
||||||
|
Long creatorId = checkNoteIsExistAndGetCreatorId(noteId);
|
||||||
|
|
||||||
|
// 2. 校验笔记是否被收藏过
|
||||||
|
// 当前登录用户ID
|
||||||
|
Long userId = LoginUserContextHolder.getUserId();
|
||||||
|
|
||||||
|
// 布隆过滤器Key
|
||||||
|
String bloomUserNoteCollectListKey = RedisKeyConstants.buildBloomUserNoteCollectListKey(userId);
|
||||||
|
|
||||||
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||||
|
// Lua 脚本路径
|
||||||
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_uncollect_check.lua")));
|
||||||
|
// 返回值类型
|
||||||
|
script.setResultType(Long.class);
|
||||||
|
|
||||||
|
// 执行 Lua 脚本,拿到返回结果
|
||||||
|
Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteCollectListKey), noteId);
|
||||||
|
|
||||||
|
NoteUnCollectLuaResultEnum noteUnCollectLuaResultEnum = NoteUnCollectLuaResultEnum.valueOf(result);
|
||||||
|
|
||||||
|
switch (Objects.requireNonNull(noteUnCollectLuaResultEnum)) {
|
||||||
|
// 布隆过滤器不存在
|
||||||
|
case NOT_EXIST -> {
|
||||||
|
// 异步初始化布隆过滤器
|
||||||
|
threadPoolTaskExecutor.submit(() -> {
|
||||||
|
// 保底1天+随机秒数
|
||||||
|
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||||
|
batchAddNoteCollect2BloomAndExpire(userId, expireSeconds, bloomUserNoteCollectListKey);
|
||||||
|
});
|
||||||
|
// 从数据库中校验笔记是否被收藏
|
||||||
|
long count = noteCollectionDOService.count(new LambdaQueryWrapper<>(NoteCollectionDO.class)
|
||||||
|
.eq(NoteCollectionDO::getUserId, userId)
|
||||||
|
.eq(NoteCollectionDO::getNoteId, noteId)
|
||||||
|
.eq(NoteCollectionDO::getStatus, CollectStatusEnum.COLLECT.getCode()));
|
||||||
|
if (count == 0) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 布隆过滤器校验目标笔记未被收藏(判断绝对正确)
|
||||||
|
case NOTE_NOT_COLLECTED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_COLLECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 删除 ZSET 中已收藏的笔记 ID
|
||||||
|
// 能走到这里,说明布隆过滤器判断已收藏,直接删除 ZSET 中已收藏的笔记 ID
|
||||||
|
// 用户收藏列表 ZSet Key
|
||||||
|
String userNoteCollectZSetKey = RedisKeyConstants.buildUserNoteCollectZSetKey(userId);
|
||||||
|
|
||||||
|
redisTemplate.opsForZSet().remove(userNoteCollectZSetKey, noteId);
|
||||||
|
|
||||||
|
// 4. 发送 MQ, 数据更新落库
|
||||||
|
// 构建消息体 DTO
|
||||||
|
CollectUnCollectNoteMqDTO unCollectNoteMqDTO = CollectUnCollectNoteMqDTO.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.noteId(noteId)
|
||||||
|
.type(CollectUnCollectNoteTypeEnum.UN_COLLECT.getCode()) // 取消收藏笔记
|
||||||
|
.createTime(LocalDateTime.now())
|
||||||
|
.noteCreatorId(creatorId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(unCollectNoteMqDTO))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||||
|
String destination = MQConstants.TOPIC_COLLECT_OR_UN_COLLECT + ":" + MQConstants.TAG_UN_COLLECT;
|
||||||
|
|
||||||
|
String hashKey = String.valueOf(userId);
|
||||||
|
|
||||||
|
// 异步发送顺序 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
|
||||||
*
|
*
|
||||||
@@ -1149,17 +1296,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);
|
||||||
@@ -1171,10 +1318,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);
|
||||||
}
|
}
|
||||||
// 缓存
|
// 缓存
|
||||||
@@ -1182,8 +1334,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -21,4 +21,14 @@
|
|||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
create_time = #{createTime}, status = #{status};
|
create_time = #{createTime}, status = #{status};
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="update2UnCollectByUserIdAndNoteId"
|
||||||
|
parameterType="com.hanserwei.hannote.note.biz.domain.dataobject.NoteCollectionDO">
|
||||||
|
update t_note_collection
|
||||||
|
set status = #{status},
|
||||||
|
create_time = #{createTime}
|
||||||
|
where user_id = #{userId}
|
||||||
|
and note_id = #{noteId}
|
||||||
|
and status = 1
|
||||||
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ Authorization: Bearer {{token}}
|
|||||||
"imgUris": [
|
"imgUris": [
|
||||||
"https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg"
|
"https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg"
|
||||||
],
|
],
|
||||||
"title": "图文笔记测试",
|
"title": "第三篇图文笔记",
|
||||||
"content": "这个是图文笔记的测试",
|
"content": "这个是第三篇图文笔记的测试",
|
||||||
"topicId": 1
|
"topicId": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +120,15 @@ Authorization: Bearer {{token}}
|
|||||||
"topicId": 1
|
"topicId": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### 删除笔记
|
||||||
|
POST http://localhost:8000/note/note/delete
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1979849112022941780
|
||||||
|
}
|
||||||
|
|
||||||
### 关注自己
|
### 关注自己
|
||||||
POST http://localhost:8000/relation/relation/follow
|
POST http://localhost:8000/relation/relation/follow
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -208,6 +217,15 @@ POST http://localhost:8000/note/note/collect
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer {{token}}
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1977249693272375330
|
||||||
|
}
|
||||||
|
|
||||||
|
### 笔记取消收藏入口
|
||||||
|
POST http://localhost:8000/note/note/uncollect
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": 1977249693272375330
|
"id": 1977249693272375330
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user