feat(note): 实现笔记取消点赞功能
- 新增 Lua 脚本用于布隆过滤器校验笔记是否被点赞 - 添加取消点赞接口 /note/note/unlike - 实现取消点赞业务逻辑,包括 Redis ZSet 删除与 MQ 异步更新 - 新增取消点赞请求 VO 类 UnlikeNoteReqVO - 新增 Lua 脚本执行结果枚举 NoteUnlikeLuaResultEnum - 添加响应码 NOTE_NOT_LIKED 用于未点赞提示 - 更新 HTTP 客户端测试用例,增加取消点赞入口 - 消费者 LikeUnlikeNoteConsumer 支持处理取消点赞消息 - 补充相关服务层方法 unlikeNote 及其实现
This commit is contained in:
@@ -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<Message> {
|
||||
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<Message> {
|
||||
* @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<NoteLikeDO> 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: 删除计数
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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", "您未点赞该篇笔记,无法取消点赞"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -57,4 +57,12 @@ public interface NoteService extends IService<NoteDO> {
|
||||
*/
|
||||
Response<?> likeNote(LikeNoteReqVO likeNoteReqVO);
|
||||
|
||||
/**
|
||||
* 取消点赞笔记
|
||||
*
|
||||
* @param unlikeNoteReqVO 取消点赞笔记请求
|
||||
* @return 取消点赞笔记结果
|
||||
*/
|
||||
Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO);
|
||||
|
||||
}
|
||||
@@ -705,6 +705,93 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> 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<Long> 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<String> 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
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user