diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/LikeUnlikeNoteConsumer.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/LikeUnlikeNoteConsumer.java index 78a2f09..07d48de 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/LikeUnlikeNoteConsumer.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/comsumer/LikeUnlikeNoteConsumer.java @@ -1,11 +1,14 @@ package com.hanserwei.hannote.note.biz.comsumer; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.google.common.util.concurrent.RateLimiter; import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.hannote.note.biz.constant.MQConstants; import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO; import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper; +import com.hanserwei.hannote.note.biz.enums.LikeUnlikeNoteTypeEnum; import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO; +import com.hanserwei.hannote.note.biz.service.NoteLikeDOService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.common.message.Message; @@ -17,7 +20,7 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Objects; -@SuppressWarnings("UnstableApiUsage") +@SuppressWarnings({"UnstableApiUsage"}) @Component @RocketMQMessageListener( consumerGroup = "han_note_" + MQConstants.TOPIC_LIKE_OR_UNLIKE, @@ -31,6 +34,8 @@ public class LikeUnlikeNoteConsumer implements RocketMQListener { private final RateLimiter rateLimiter = RateLimiter.create(5000); @Resource private NoteLikeDOMapper noteLikeDOMapper; + @Resource + private NoteLikeDOService noteLikeDOService; @Override public void onMessage(Message message) { @@ -60,7 +65,36 @@ public class LikeUnlikeNoteConsumer implements RocketMQListener { * @param bodyJsonStr 消息体 */ private void handleUnlikeNoteTagMessage(String bodyJsonStr) { + // 消息体 JSON 字符串转 DTO + LikeUnlikeNoteMqDTO unlikeNoteMqDTO = JsonUtils.parseObject(bodyJsonStr, LikeUnlikeNoteMqDTO.class); + if (Objects.isNull(unlikeNoteMqDTO)) { + return; + } + // 用户ID + Long userId = unlikeNoteMqDTO.getUserId(); + // 点赞的笔记ID + Long noteId = unlikeNoteMqDTO.getNoteId(); + // 操作类型 + Integer type = unlikeNoteMqDTO.getType(); + // 取消点赞时间 + LocalDateTime createTime = unlikeNoteMqDTO.getCreateTime(); + // 设置要更新的字段值 + NoteLikeDO updateEntity = NoteLikeDO.builder() + .createTime(createTime) // 更新时间 + .status(type) // 设置新的状态值 (例如 0 表示取消点赞) + .build(); + + // 设置更新条件:where user_id = [userId] and note_id = [noteId] and status = 1 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(NoteLikeDO::getUserId, userId) + .eq(NoteLikeDO::getNoteId, noteId) + .eq(NoteLikeDO::getStatus, LikeUnlikeNoteTypeEnum.LIKE.getCode()); // 确保只更新当前为“已点赞”的记录 + + // 执行更新 + boolean update = noteLikeDOService.update(updateEntity, wrapper); + + // TODO: 删除计数 } /** 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 f40c950..b553a5c 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 @@ -62,4 +62,10 @@ public class NoteController { return noteService.likeNote(likeNoteReqVO); } + @PostMapping(value = "/unlike") + @ApiOperationLog(description = "取消点赞笔记") + public Response unlikeNote(@Validated @RequestBody UnlikeNoteReqVO unlikeNoteReqVO) { + return noteService.unlikeNote(unlikeNoteReqVO); + } + } diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnlikeLuaResultEnum.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnlikeLuaResultEnum.java new file mode 100644 index 0000000..25f8f1a --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/enums/NoteUnlikeLuaResultEnum.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 NoteUnlikeLuaResultEnum { + // 布隆过滤器不存在 + NOT_EXIST(-1L), + // 笔记已点赞 + NOTE_LIKED(1L), + // 笔记未点赞 + NOTE_NOT_LIKED(0L), + ; + + private final Long code; + + /** + * 根据类型 code 获取对应的枚举 + * + * @param code 类型 code + * @return 枚举 + */ + public static NoteUnlikeLuaResultEnum valueOf(Long code) { + for (NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum : NoteUnlikeLuaResultEnum.values()) { + if (Objects.equals(code, noteUnlikeLuaResultEnum.getCode())) { + return noteUnlikeLuaResultEnum; + } + } + return null; + } +} 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 b03f3c2..7913188 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 @@ -22,6 +22,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface { NOTE_CANT_VISIBLE_ONLY_ME("NOTE-20006", "此笔记无法修改为仅自己可见"), NOTE_CANT_OPERATE("NOTE-20007", "您无法操作该笔记"), NOTE_ALREADY_LIKED("NOTE-20008", "您已经点赞过该笔记"), + NOTE_NOT_LIKED("NOTE-20009", "您未点赞该篇笔记,无法取消点赞"), ; // 异常码 diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnlikeNoteReqVO.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnlikeNoteReqVO.java new file mode 100644 index 0000000..9adebe2 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/model/vo/UnlikeNoteReqVO.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 UnlikeNoteReqVO { + + @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 e17e842..caaa479 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 @@ -57,4 +57,12 @@ public interface NoteService extends IService { */ Response likeNote(LikeNoteReqVO likeNoteReqVO); + /** + * 取消点赞笔记 + * + * @param unlikeNoteReqVO 取消点赞笔记请求 + * @return 取消点赞笔记结果 + */ + Response unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO); + } \ 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 a0b31f0..57046c0 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 @@ -705,6 +705,93 @@ public class NoteServiceImpl extends ServiceImpl implement return Response.success(); } + @Override + public Response unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) { + // 笔记ID + Long noteId = unlikeNoteReqVO.getId(); + + // 1. 校验笔记是否真实存在 + checkNoteIsExist(noteId); + + // 2. 校验笔记是否被点赞过 + // 当前登录用户ID + Long userId = LoginUserContextHolder.getUserId(); + + // 布隆过滤器Key + String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId); + + DefaultRedisScript script = new DefaultRedisScript<>(); + // Lua 脚本路径 + script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua"))); + script.setResultType(Long.class); + + // 执行 Lua 脚本,拿到返回结果 + Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId); + + NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result); + log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum); + assert noteUnlikeLuaResultEnum != null; + switch (noteUnlikeLuaResultEnum) { + case NOT_EXIST -> { + //笔记不存在 + //异步初始化布隆过滤器 + threadPoolTaskExecutor.submit(() -> { + // 保底1天+随机秒数 + long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24); + batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey); + // 从数据库中校验笔记是否被点赞 + long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class) + .eq(NoteLikeDO::getUserId, userId) + .eq(NoteLikeDO::getNoteId, noteId) + .eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode())); + if (count == 0) { + log.info("==> 【笔记取消点赞】用户未点赞该笔记"); + throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED); + } + }); + } + case NOTE_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED); + } + + // 3. 能走到这里,说明布隆过滤器判断已点赞,直接删除 ZSET 中已点赞的笔记 ID + // 用户点赞列表ZsetKey + String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId); + + redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId); + + //4. 发送 MQ, 数据更新落库 + // 构建MQ消息体 + LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder() + .userId(userId) + .noteId(noteId) + .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode()) // 取消点赞笔记 + .createTime(LocalDateTime.now()) + .build(); + + // 构建消息,将DTO转换为JSON字符串设置到消息体中 + Message message = MessageBuilder.withPayload(JsonUtils.toJsonString(likeUnlikeNoteMqDTO)).build(); + + // 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag + String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE; + + String hashKey = String.valueOf(noteId); + + // 异步发送 MQ 消息,提升接口响应速度 + rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("==> 【笔记取消点赞】MQ 发送成功,SendResult: {}", sendResult); + } + + @Override + public void onException(Throwable throwable) { + log.error("==> 【笔记取消点赞】MQ 发送异常: ", throwable); + } + }); + + return Response.success(); + } + /** * 异步初始化用户点赞笔记 ZSet * diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_unlike_check.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_unlike_check.lua new file mode 100644 index 0000000..9adfd2f --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/bloom_note_unlike_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/http-client/gateApi.http b/http-client/gateApi.http index 2d83335..13a1aed 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -191,5 +191,14 @@ Content-Type: application/json Authorization: Bearer {{token}} { - "id": {{noteId}} + "id": 1977249693272375330 +} + +### 笔记取消点赞入口 +POST http://localhost:8000/note/note/unlike +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "id": 1977249693272375330 } \ No newline at end of file