feat(note): 新增笔记删除功能
- 新增 DeleteNoteReqVO 请求参数类,用于接收笔记删除请求 - 在 NoteController 中添加 /delete 接口,实现笔记删除功能 - 在 NoteService 和 NoteServiceImpl 中实现 deleteNote 方法 - 删除笔记时进行权限校验,仅允许笔记创建者删除 - 删除操作为逻辑删除,更新笔记状态为已删除 - 删除笔记后清除 Redis 缓存,并通过 MQ 广播通知各实例清除本地缓存 -优化更新和可见性接口的权限校验逻辑,避免重复代码 - 添加 MQ 测试类 MQTests,用于批量发送关注/取关消息 - 引入 Guava 的 RateLimiter 实现 MQ 消费端限流- 配置 Nacos 配置中心依赖及动态刷新配置 - 更新 .gitignore 文件,忽略日志文件目录 - 在 application.yml 中添加 MQ 消费者限流配置项 - 在 bootstrap.yml 中完善 Nacos 配置中心相关配置 - 为 FollowUnfollowConsumer 添加限流逻辑,防止消费端压力过大
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -50,3 +50,4 @@ build/
|
|||||||
/han-note-kv/han-note-kv-biz/logs/
|
/han-note-kv/han-note-kv-biz/logs/
|
||||||
/han-note-note/han-note-note-biz/src/main/resources/application-dev.yml
|
/han-note-note/han-note-note-biz/src/main/resources/application-dev.yml
|
||||||
/han-note-user-relation/han-note-user-relation-biz/src/main/resources/application-dev.yml
|
/han-note-user-relation/han-note-user-relation-biz/src/main/resources/application-dev.yml
|
||||||
|
/han-note-user-relation/han-note-user-relation-biz/logs/
|
||||||
|
|||||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
5
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +1,11 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<Languages>
|
||||||
|
<language minSize="56" name="Java" />
|
||||||
|
</Languages>
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="customHeaders">
|
<option name="customHeaders">
|
||||||
<set>
|
<set>
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ public class NoteController {
|
|||||||
return noteService.updateNote(updateNoteReqVO);
|
return noteService.updateNote(updateNoteReqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/delete")
|
||||||
|
@ApiOperationLog(description = "笔记删除")
|
||||||
|
public Response<?> deleteNote(@Validated @RequestBody DeleteNoteReqVO deleteNoteReqVO) {
|
||||||
|
return noteService.deleteNote(deleteNoteReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/visible/onlyme")
|
@PostMapping(value = "/visible/onlyme")
|
||||||
@ApiOperationLog(description = "笔记仅对自己可见")
|
@ApiOperationLog(description = "笔记仅对自己可见")
|
||||||
public Response<?> visibleOnlyMe(@Validated @RequestBody UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
public Response<?> visibleOnlyMe(@Validated @RequestBody UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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 DeleteNoteReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "笔记 ID 不能为空")
|
||||||
|
private Long id;
|
||||||
|
}
|
||||||
@@ -28,6 +28,13 @@ public interface NoteService extends IService<NoteDO> {
|
|||||||
*/
|
*/
|
||||||
Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO);
|
Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 笔记删除
|
||||||
|
* @param deleteNoteReqVO 笔记删除请求
|
||||||
|
* @return 笔记删除结果
|
||||||
|
*/
|
||||||
|
Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 笔记仅对自己可见
|
* 笔记仅对自己可见
|
||||||
* @param updateNoteVisibleOnlyMeReqVO 笔记仅对自己可见请求
|
* @param updateNoteVisibleOnlyMeReqVO 笔记仅对自己可见请求
|
||||||
|
|||||||
@@ -334,6 +334,20 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当前登录用户 ID
|
||||||
|
Long currUserId = LoginUserContextHolder.getUserId();
|
||||||
|
NoteDO selectNoteDO = this.getById(noteId);
|
||||||
|
|
||||||
|
// 笔记不存在
|
||||||
|
if (Objects.isNull(selectNoteDO)) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断权限:非笔记发布者不允许更新笔记
|
||||||
|
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||||
|
}
|
||||||
|
|
||||||
// 话题
|
// 话题
|
||||||
Long topicId = updateNoteReqVO.getTopicId();
|
Long topicId = updateNoteReqVO.getTopicId();
|
||||||
String topicName = null;
|
String topicName = null;
|
||||||
@@ -428,11 +442,68 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
|
||||||
|
// 笔记 ID
|
||||||
|
Long noteId = deleteNoteReqVO.getId();
|
||||||
|
|
||||||
|
NoteDO selectNoteDO = this.getById(noteId);
|
||||||
|
|
||||||
|
// 判断笔记是否存在
|
||||||
|
if (Objects.isNull(selectNoteDO)) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断权限:非笔记发布者不允许删除笔记
|
||||||
|
Long currUserId = LoginUserContextHolder.getUserId();
|
||||||
|
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逻辑删除
|
||||||
|
NoteDO noteDO = NoteDO.builder()
|
||||||
|
.id(noteId)
|
||||||
|
.status(NoteStatusEnum.DELETED.getCode())
|
||||||
|
.updateTime(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
boolean updateSuccess = this.updateById(noteDO);
|
||||||
|
|
||||||
|
// 若失败,则表示该笔记不存在
|
||||||
|
if (!updateSuccess) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除缓存
|
||||||
|
String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
|
||||||
|
redisTemplate.delete(noteDetailRedisKey);
|
||||||
|
|
||||||
|
// 同步发送广播模式 MQ,将所有实例中的本地缓存都删除掉
|
||||||
|
rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
|
||||||
|
log.info("====> MQ:删除笔记,删除本地缓存发送成功...");
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
||||||
// 笔记 ID
|
// 笔记 ID
|
||||||
Long noteId = updateNoteVisibleOnlyMeReqVO.getId();
|
Long noteId = updateNoteVisibleOnlyMeReqVO.getId();
|
||||||
|
|
||||||
|
NoteDO selectNoteDO = this.getById(noteId);
|
||||||
|
|
||||||
|
// 判断笔记是否存在
|
||||||
|
if (Objects.isNull(selectNoteDO)) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断权限:非笔记发布者不允许修改笔记权限
|
||||||
|
Long currUserId = LoginUserContextHolder.getUserId();
|
||||||
|
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||||
|
}
|
||||||
|
|
||||||
// 构建更新的实体类
|
// 构建更新的实体类
|
||||||
NoteDO noteDO = NoteDO.builder()
|
NoteDO noteDO = NoteDO.builder()
|
||||||
.id(noteId)
|
.id(noteId)
|
||||||
|
|||||||
@@ -102,6 +102,12 @@
|
|||||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- NaCos 配置中心 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.hanserwei.hannote.user.relation.biz.config;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RefreshScope
|
||||||
|
public class FollowUnfollowMqConsumerRateLimitConfig {
|
||||||
|
|
||||||
|
@Value("${mq-consumer.follow-unfollow.rate-limit}")
|
||||||
|
private double rateLimit;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@RefreshScope
|
||||||
|
public RateLimiter rateLimiter() {
|
||||||
|
return RateLimiter.create(rateLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.hanserwei.hannote.user.relation.biz.consumer;
|
package com.hanserwei.hannote.user.relation.biz.consumer;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||||
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
|
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
|
||||||
@@ -7,6 +8,8 @@ import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
|
|||||||
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||||
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
|
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
|
||||||
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
|
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.rocketmq.common.message.Message;
|
import org.apache.rocketmq.common.message.Message;
|
||||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||||
@@ -23,19 +26,19 @@ import java.util.Objects;
|
|||||||
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW
|
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW
|
||||||
)
|
)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||||
private final TransactionTemplate transactionTemplate;
|
private final TransactionTemplate transactionTemplate;
|
||||||
private final FollowingDOService followingDOService;
|
private final FollowingDOService followingDOService;
|
||||||
private final FansDOService fansDOService;
|
private final FansDOService fansDOService;
|
||||||
|
|
||||||
public FollowUnfollowConsumer(TransactionTemplate transactionTemplate, FollowingDOService followingDOService, FansDOService fansDOService) {
|
@Resource
|
||||||
this.transactionTemplate = transactionTemplate;
|
private RateLimiter rateLimiter;
|
||||||
this.followingDOService = followingDOService;
|
|
||||||
this.fansDOService = fansDOService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(Message message) {
|
public void onMessage(Message message) {
|
||||||
|
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
|
||||||
|
rateLimiter.acquire();
|
||||||
// 消息体
|
// 消息体
|
||||||
String bodyJsonStr = new String(message.getBody());
|
String bodyJsonStr = new String(message.getBody());
|
||||||
// 标签
|
// 标签
|
||||||
|
|||||||
@@ -28,4 +28,7 @@ mybatis-plus:
|
|||||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
global-config:
|
global-config:
|
||||||
banner: false
|
banner: false
|
||||||
mapper-locations: classpath*:/mapperxml/*.xml
|
mapper-locations: classpath*:/mapperxml/*.xml
|
||||||
|
mq-consumer: # MQ 消费者
|
||||||
|
follow-unfollow: # 关注、取关
|
||||||
|
rate-limit: 5000 # 每秒限流阈值
|
||||||
@@ -10,3 +10,10 @@ spring:
|
|||||||
group: DEFAULT_GROUP # 所属组
|
group: DEFAULT_GROUP # 所属组
|
||||||
namespace: han-note # 命名空间
|
namespace: han-note # 命名空间
|
||||||
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
|
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
|
||||||
|
config:
|
||||||
|
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
|
||||||
|
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
|
||||||
|
group: DEFAULT_GROUP # 所属组
|
||||||
|
namespace: han-note # 命名空间
|
||||||
|
file-extension: yaml # 配置文件格式
|
||||||
|
refresh-enabled: true # 是否开启动态刷新
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.hanserwei.hannote.user.relation.biz;
|
||||||
|
|
||||||
|
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||||
|
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||||
|
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.rocketmq.client.producer.SendCallback;
|
||||||
|
import org.apache.rocketmq.client.producer.SendResult;
|
||||||
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Slf4j
|
||||||
|
class MQTests {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RocketMQTemplate rocketMQTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试:发送一万条 MQ
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testBatchSendMQ() {
|
||||||
|
for (long i = 0; i < 10000; i++) {
|
||||||
|
// 构建消息体 DTO
|
||||||
|
FollowUserMqDTO followUserMqDTO = FollowUserMqDTO.builder()
|
||||||
|
.userId(i)
|
||||||
|
.followUserId(i)
|
||||||
|
.createTime(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(followUserMqDTO))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||||
|
String destination = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW + ":" + MQConstants.TAG_FOLLOW;
|
||||||
|
|
||||||
|
log.info("==> 开始发送关注操作 MQ, 消息体: {}", followUserMqDTO);
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息,提升接口响应速度
|
||||||
|
rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user