feat(note): 实现笔记取消收藏功能
- 新增取消收藏笔记的 Controller 接口 /uncollect - 实现取消收藏笔记的业务逻辑,包括布隆过滤器校验和数据库状态更新 - 添加 Lua 脚本用于 Redis 布隆过滤器检查笔记是否被收藏 - 新增取消收藏相关的枚举类 NoteUnCollectLuaResultEnum - 扩展 RocketMQ 消息标签支持取消收藏操作 - 在 NoteCollectionDOMapper 中新增 update2UnCollectByUserIdAndNoteId 方法 - 新增响应码 NOTE_NOT_COLLECTED用于未收藏情况的错误提示 - 添加取消收藏请求参数 VO 类 UnCollectNoteReqVO - 更新 HTTP 客户端测试脚本增加取消收藏接口调用示例
This commit is contained in:
@@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@SuppressWarnings({"UnstableApiUsage", "DuplicatedCode"})
|
||||
@Component
|
||||
@Slf4j
|
||||
@RocketMQMessageListener(
|
||||
@@ -60,7 +60,32 @@ public class CollectUnCollectNoteConsumer implements RocketMQListener<Message> {
|
||||
* @param 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);
|
||||
|
||||
// TODO: 发送计数 MQ
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,11 @@ public interface MQConstants {
|
||||
*/
|
||||
String TOPIC_COLLECT_OR_UN_COLLECT = "CollectUnCollectTopic";
|
||||
|
||||
/**
|
||||
* Topic: 计数 - 笔记收藏数
|
||||
*/
|
||||
String TOPIC_COUNT_NOTE_COLLECT = "CountNoteCollectTopic";
|
||||
|
||||
/**
|
||||
* 点赞标签
|
||||
*/
|
||||
|
||||
@@ -74,4 +74,10 @@ public class NoteController {
|
||||
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 是否成功
|
||||
*/
|
||||
boolean insertOrUpdate(NoteCollectionDO noteCollectionDO);
|
||||
|
||||
/**
|
||||
* 取消点赞
|
||||
*
|
||||
* @param noteCollectionDO 笔记收藏记录
|
||||
* @return 影响行数
|
||||
*/
|
||||
int update2UnCollectByUserIdAndNoteId(NoteCollectionDO noteCollectionDO);
|
||||
}
|
||||
@@ -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_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"),
|
||||
NOTE_ALREADY_COLLECTED("NOTE-20010", "您已经收藏过该笔记"),
|
||||
NOTE_NOT_COLLECTED("NOTE-20011", "您未收藏该篇笔记,无法取消收藏"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 取消收藏笔记
|
||||
*
|
||||
* @param unCollectNoteReqVO 取消收藏笔记请求
|
||||
* @return 取消收藏笔记结果
|
||||
*/
|
||||
Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO);
|
||||
|
||||
}
|
||||
@@ -959,6 +959,95 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<?> unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO) {
|
||||
// 笔记ID
|
||||
Long noteId = unCollectNoteReqVO.getId();
|
||||
|
||||
// 1. 校验笔记是否真实存在
|
||||
checkNoteIsExist(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())
|
||||
.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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
create_time = #{createTime}, status = #{status};
|
||||
</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>
|
||||
Reference in New Issue
Block a user