From c036fadbffb32fb3f0473aad0fd04d8dce7c82f2 Mon Sep 17 00:00:00 2001 From: Hanserwei <2628273921@qq.com> Date: Sun, 19 Oct 2025 15:40:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(note):=20=E5=AE=9E=E7=8E=B0=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20-=20=E6=96=B0=E5=A2=9E=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E7=AC=94=E8=AE=B0=E7=9A=84=20Controller=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20/uncollect=20-=20=E5=AE=9E=E7=8E=B0=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E7=AC=94=E8=AE=B0=E7=9A=84=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=8C=85=E6=8B=AC=E5=B8=83=E9=9A=86?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E6=A0=A1=E9=AA=8C=E5=92=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0=20-=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Lua=20=E8=84=9A=E6=9C=AC=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=20Redis=20=E5=B8=83=E9=9A=86=E8=BF=87=E6=BB=A4=E5=99=A8?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E7=AC=94=E8=AE=B0=E6=98=AF=E5=90=A6=E8=A2=AB?= =?UTF-8?q?=E6=94=B6=E8=97=8F=20-=20=E6=96=B0=E5=A2=9E=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E7=9B=B8=E5=85=B3=E7=9A=84=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E7=B1=BB=20NoteUnCollectLuaResultEnum=20-=20=E6=89=A9=E5=B1=95?= =?UTF-8?q?=20RocketMQ=20=E6=B6=88=E6=81=AF=E6=A0=87=E7=AD=BE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=20-=20=E5=9C=A8=20NoteCollectionDOMapper=20=E4=B8=AD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20update2UnCollectByUserIdAndNoteId=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=20-=20=E6=96=B0=E5=A2=9E=E5=93=8D=E5=BA=94=E7=A0=81?= =?UTF-8?q?=20NOTE=5FNOT=5FCOLLECTED=E7=94=A8=E4=BA=8E=E6=9C=AA=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E6=83=85=E5=86=B5=E7=9A=84=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=20-=20=E6=B7=BB=E5=8A=A0=E5=8F=96=E6=B6=88=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0=20VO=20=E7=B1=BB?= =?UTF-8?q?=20UnCollectNoteReqVO=20-=20=E6=9B=B4=E6=96=B0=20HTTP=20?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=B5=8B=E8=AF=95=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E8=B0=83=E7=94=A8=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectUnCollectNoteConsumer.java | 27 +++++- .../note/biz/constant/MQConstants.java | 5 ++ .../note/biz/controller/NoteController.java | 6 ++ .../domain/mapper/NoteCollectionDOMapper.java | 8 ++ .../biz/enums/NoteUnCollectLuaResultEnum.java | 35 ++++++++ .../note/biz/enums/ResponseCodeEnum.java | 1 + .../note/biz/model/vo/UnCollectNoteReqVO.java | 18 ++++ .../hannote/note/biz/service/NoteService.java | 8 ++ .../biz/service/impl/NoteServiceImpl.java | 89 +++++++++++++++++++ .../lua/bloom_note_uncollect_check.lua | 11 +++ .../mapperxml/NoteCollectionDOMapper.xml | 10 +++ http-client/gateApi.http | 9 ++ 12 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnCollectLuaResultEnum.java create mode 100644 han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnCollectNoteReqVO.java create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_uncollect_check.lua 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