diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/CollectUnCollectNoteConsumer.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/CollectUnCollectNoteConsumer.java index 7cbb117..dab7996 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/CollectUnCollectNoteConsumer.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/CollectUnCollectNoteConsumer.java @@ -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 { * @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 } /** diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/MQConstants.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/MQConstants.java index 0832f2d..4ae7ef4 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/MQConstants.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/MQConstants.java @@ -27,6 +27,11 @@ public interface MQConstants { */ String TOPIC_COLLECT_OR_UN_COLLECT = "CollectUnCollectTopic"; + /** + * Topic: 计数 - 笔记收藏数 + */ + String TOPIC_COUNT_NOTE_COLLECT = "CountNoteCollectTopic"; + /** * 点赞标签 */ diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/controller/NoteController.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/controller/NoteController.java index 5b0fe80..62324c3 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/controller/NoteController.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/controller/NoteController.java @@ -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); + } + } diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/domain/mapper/NoteCollectionDOMapper.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/domain/mapper/NoteCollectionDOMapper.java index f9e3b0a..0379ba2 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/domain/mapper/NoteCollectionDOMapper.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/domain/mapper/NoteCollectionDOMapper.java @@ -13,4 +13,12 @@ public interface NoteCollectionDOMapper extends BaseMapper { * @return 是否成功 */ boolean insertOrUpdate(NoteCollectionDO noteCollectionDO); + + /** + * 取消点赞 + * + * @param noteCollectionDO 笔记收藏记录 + * @return 影响行数 + */ + int update2UnCollectByUserIdAndNoteId(NoteCollectionDO noteCollectionDO); } \ No newline at end of file diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnCollectLuaResultEnum.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnCollectLuaResultEnum.java new file mode 100644 index 0000000..b576288 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnCollectLuaResultEnum.java @@ -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; + } +} \ No newline at end of file diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/ResponseCodeEnum.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/ResponseCodeEnum.java index e8723f3..a58bf57 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/ResponseCodeEnum.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/ResponseCodeEnum.java @@ -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", "您未收藏该篇笔记,无法取消收藏"), ; // 异常码 diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnCollectNoteReqVO.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnCollectNoteReqVO.java new file mode 100644 index 0000000..c88fc4e --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnCollectNoteReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/NoteService.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/NoteService.java index fee502c..303590f 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/NoteService.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/NoteService.java @@ -73,4 +73,12 @@ public interface NoteService extends IService { */ Response collectNote(CollectNoteReqVO collectNoteReqVO); + /** + * 取消收藏笔记 + * + * @param unCollectNoteReqVO 取消收藏笔记请求 + * @return 取消收藏笔记结果 + */ + Response unCollectNote(UnCollectNoteReqVO unCollectNoteReqVO); + } \ No newline at end of file diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java index 7b22e1a..24ae338 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java @@ -959,6 +959,95 @@ public class NoteServiceImpl extends ServiceImpl 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 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 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 * diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_uncollect_check.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_uncollect_check.lua new file mode 100644 index 0000000..2f4c9e4 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_uncollect_check.lua @@ -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) diff --git a/han-note-note/han-note-note-biz/src/main/resources/mapperxml/NoteCollectionDOMapper.xml b/han-note-note/han-note-note-biz/src/main/resources/mapperxml/NoteCollectionDOMapper.xml index 886f334..b59931d 100644 --- a/han-note-note/han-note-note-biz/src/main/resources/mapperxml/NoteCollectionDOMapper.xml +++ b/han-note-note/han-note-note-biz/src/main/resources/mapperxml/NoteCollectionDOMapper.xml @@ -21,4 +21,14 @@ ON DUPLICATE KEY UPDATE create_time = #{createTime}, status = #{status}; + + + update t_note_collection + set status = #{status}, + create_time = #{createTime} + where user_id = #{userId} + and note_id = #{noteId} + and status = 1 + \ No newline at end of file diff --git a/http-client/gateApi.http b/http-client/gateApi.http index 16a07cc..690198a 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -208,6 +208,15 @@ POST http://localhost:8000/note/note/collect Content-Type: application/json Authorization: Bearer {{token}} +{ + "id": 1977249693272375330 +} + +### 笔记取消收藏入口 +POST http://localhost:8000/note/note/uncollect +Content-Type: application/json +Authorization: Bearer {{token}} + { "id": 1977249693272375330 } \ No newline at end of file