feat(comment): 新增删除评论功能
- 新增删除评论接口,支持物理删除评论及关联内容 - 添加权限校验,仅允许评论创建者删除评论- 使用编程式事务保证删除操作的原子性- 删除评论后清理 Redis 缓存(ZSet 和 String 类型) - 发送 MQ 消息异步更新计数、删除关联数据及本地缓存 - 新增 DeleteCommentReqVO 请求参数类校验评论 ID - 补充 KeyValueRpcService 删除评论内容方法 - 新增相关 MQ Topic 常量及响应码枚举 - 更新 HTTP 接口测试用例
This commit is contained in:
@@ -22,6 +22,16 @@ public interface MQConstants {
|
|||||||
*/
|
*/
|
||||||
String TOPIC_COMMENT_LIKE_OR_UNLIKE = "CommentLikeUnlikeTopic";
|
String TOPIC_COMMENT_LIKE_OR_UNLIKE = "CommentLikeUnlikeTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 删除本地缓存 —— 评论详情
|
||||||
|
*/
|
||||||
|
String TOPIC_DELETE_COMMENT_LOCAL_CACHE = "DeleteCommentDetailLocalCacheTopic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topic: 删除评论
|
||||||
|
*/
|
||||||
|
String TOPIC_DELETE_COMMENT = "DeleteCommentTopic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag 标签:点赞
|
* Tag 标签:点赞
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -51,4 +51,10 @@ public class CommentController {
|
|||||||
return commentService.unlikeComment(unLikeCommentReqVO);
|
return commentService.unlikeComment(unLikeCommentReqVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@ApiOperationLog(description = "删除评论")
|
||||||
|
public Response<?> deleteComment(@Validated @RequestBody DeleteCommentReqVO deleteCommentReqVO) {
|
||||||
|
return commentService.deleteComment(deleteCommentReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
|||||||
PARENT_COMMENT_NOT_FOUND("COMMENT-20000", "此父评论不存在"),
|
PARENT_COMMENT_NOT_FOUND("COMMENT-20000", "此父评论不存在"),
|
||||||
COMMENT_ALREADY_LIKED("COMMENT-20002", "您已经点赞过该评论"),
|
COMMENT_ALREADY_LIKED("COMMENT-20002", "您已经点赞过该评论"),
|
||||||
COMMENT_NOT_LIKED("COMMENT-20003", "您未点赞该评论,无法取消点赞"),
|
COMMENT_NOT_LIKED("COMMENT-20003", "您未点赞该评论,无法取消点赞"),
|
||||||
|
COMMENT_CANT_OPERATE("COMMENT-20004", "您无法操作该评论"),
|
||||||
;
|
;
|
||||||
|
|
||||||
// 异常码
|
// 异常码
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.hanserwei.hannote.comment.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 DeleteCommentReqVO {
|
||||||
|
|
||||||
|
@NotNull(message = "评论 ID 不能为空")
|
||||||
|
private Long commentId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,14 +6,12 @@ import com.hanserwei.framework.common.constant.DateConstants;
|
|||||||
import com.hanserwei.framework.common.response.Response;
|
import com.hanserwei.framework.common.response.Response;
|
||||||
import com.hanserwei.hannote.comment.biz.model.bo.CommentBO;
|
import com.hanserwei.hannote.comment.biz.model.bo.CommentBO;
|
||||||
import com.hanserwei.hannote.kv.api.KeyValueFeignApi;
|
import com.hanserwei.hannote.kv.api.KeyValueFeignApi;
|
||||||
import com.hanserwei.hannote.kv.dto.req.BatchAddCommentContentReqDTO;
|
import com.hanserwei.hannote.kv.dto.req.*;
|
||||||
import com.hanserwei.hannote.kv.dto.req.BatchFindCommentContentReqDTO;
|
|
||||||
import com.hanserwei.hannote.kv.dto.req.CommentContentReqDTO;
|
|
||||||
import com.hanserwei.hannote.kv.dto.req.FindCommentContentReqDTO;
|
|
||||||
import com.hanserwei.hannote.kv.dto.resp.FindCommentContentRspDTO;
|
import com.hanserwei.hannote.kv.dto.resp.FindCommentContentRspDTO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -81,4 +79,29 @@ public class KeyValueRpcService {
|
|||||||
return response.getData();
|
return response.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论内容
|
||||||
|
*
|
||||||
|
* @param noteId 笔记ID
|
||||||
|
* @param createTime 创建时间
|
||||||
|
* @param contentId 评论内容ID
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
public boolean deleteCommentContent(Long noteId, LocalDateTime createTime, String contentId) {
|
||||||
|
DeleteCommentContentReqDTO deleteCommentContentReqDTO = DeleteCommentContentReqDTO.builder()
|
||||||
|
.noteId(noteId)
|
||||||
|
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(createTime))
|
||||||
|
.contentId(contentId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 调用 KV 存储服务
|
||||||
|
Response<?> response = keyValueFeignApi.deleteCommentContent(deleteCommentContentReqDTO);
|
||||||
|
|
||||||
|
if (!response.isSuccess()) {
|
||||||
|
throw new RuntimeException("删除评论内容失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -46,4 +46,12 @@ public interface CommentService extends IService<CommentDO> {
|
|||||||
* @return 响应
|
* @return 响应
|
||||||
*/
|
*/
|
||||||
Response<?> unlikeComment(UnLikeCommentReqVO unLikeCommentReqVO);
|
Response<?> unlikeComment(UnLikeCommentReqVO unLikeCommentReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
*
|
||||||
|
* @param deleteCommentReqVO 删除评论请求
|
||||||
|
* @return 响应
|
||||||
|
*/
|
||||||
|
Response<?> deleteComment(DeleteCommentReqVO deleteCommentReqVO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import org.springframework.messaging.support.MessageBuilder;
|
|||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.scripting.support.ResourceScriptSource;
|
import org.springframework.scripting.support.ResourceScriptSource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -82,6 +83,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private CommentLikeDOMapper commentLikeDOMapper;
|
private CommentLikeDOMapper commentLikeDOMapper;
|
||||||
|
@Resource
|
||||||
|
private TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评论详情本地缓存
|
* 评论详情本地缓存
|
||||||
@@ -620,6 +623,101 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Response<?> deleteComment(DeleteCommentReqVO deleteCommentReqVO) {
|
||||||
|
// 被删除的评论 ID
|
||||||
|
Long commentId = deleteCommentReqVO.getCommentId();
|
||||||
|
|
||||||
|
// 1. 校验评论是否存在
|
||||||
|
CommentDO commentDO = commentDOMapper.selectById(commentId);
|
||||||
|
|
||||||
|
if (Objects.isNull(commentDO)) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.COMMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验是否有权限删除
|
||||||
|
Long currUserId = LoginUserContextHolder.getUserId();
|
||||||
|
if (!Objects.equals(currUserId, commentDO.getUserId())) {
|
||||||
|
throw new ApiException(ResponseCodeEnum.COMMENT_CANT_OPERATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 物理删除评论、评论内容
|
||||||
|
// 编程式事务,保证多个操作的原子性
|
||||||
|
transactionTemplate.execute(status -> {
|
||||||
|
try {
|
||||||
|
// 删除评论元数据
|
||||||
|
commentDOMapper.deleteById(commentId);
|
||||||
|
|
||||||
|
// 删除评论内容
|
||||||
|
keyValueRpcService.deleteCommentContent(commentDO.getNoteId(),
|
||||||
|
commentDO.getCreateTime(),
|
||||||
|
commentDO.getContentUuid());
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
status.setRollbackOnly(); // 标记事务为回滚
|
||||||
|
log.error("", ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 删除 Redis 缓存(ZSet 和 String)
|
||||||
|
Integer level = commentDO.getLevel();
|
||||||
|
Long noteId = commentDO.getNoteId();
|
||||||
|
Long parentCommentId = commentDO.getParentId();
|
||||||
|
|
||||||
|
// 根据评论级别,构建对应的 ZSet Key
|
||||||
|
String redisZSetKey = Objects.equals(level, 1) ?
|
||||||
|
RedisKeyConstants.buildCommentListKey(noteId) : RedisKeyConstants.buildChildCommentListKey(parentCommentId);
|
||||||
|
|
||||||
|
// 使用 RedisTemplate 执行管道操作
|
||||||
|
redisTemplate.executePipelined(new SessionCallback<>() {
|
||||||
|
@Override
|
||||||
|
public Object execute(@NonNull RedisOperations operations) {
|
||||||
|
// 删除 ZSet 中对应评论 ID
|
||||||
|
operations.opsForZSet().remove(redisZSetKey, commentId);
|
||||||
|
|
||||||
|
// 删除评论详情
|
||||||
|
operations.delete(RedisKeyConstants.buildCommentDetailKey(commentId));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 发布广播 MQ, 将本地缓存删除
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELETE_COMMENT_LOCAL_CACHE, commentId, new SendCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SendResult sendResult) {
|
||||||
|
log.info("==> 【删除评论详情本地缓存】MQ 发送成功,SendResult: {}", sendResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onException(Throwable throwable) {
|
||||||
|
log.error("==> 【删除评论详情本地缓存】MQ 发送异常: ", throwable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 发送 MQ, 异步去更新计数、删除关联评论、热度值等
|
||||||
|
// 构建消息对象,并将 DO 转成 Json 字符串设置到消息体中
|
||||||
|
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(commentDO))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 异步发送 MQ 消息,提升接口响应速度
|
||||||
|
rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELETE_COMMENT, message, 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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化评论点赞布隆过滤器
|
* 初始化评论点赞布隆过滤器
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -394,3 +394,12 @@ Content-Type: application/json
|
|||||||
"yearMonth": "2025-11",
|
"yearMonth": "2025-11",
|
||||||
"contentId": "0fa4376f-a098-4fee-821b-f5b7e627a72c"
|
"contentId": "0fa4376f-a098-4fee-821b-f5b7e627a72c"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### 删除评论,同步删除一切相关缓存
|
||||||
|
POST http://localhost:8000/comment/comment/delete
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"commentId": 8001
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user