feat(backend): Improve authorization governance and video interactions

This commit is contained in:
2026-04-24 17:34:09 +08:00
parent 6c954542ac
commit 600ff66bd3
42 changed files with 1239 additions and 74 deletions
@@ -3,6 +3,7 @@ package com.nuaa.video.comment.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.comment.entity.Comment;
import com.nuaa.video.comment.service.CommentService;
import com.nuaa.video.comment.vo.AdminCommentVO;
import com.nuaa.video.comment.vo.CommentStatVO;
import com.nuaa.video.comment.vo.CommentVO;
import com.nuaa.video.common.model.vo.Result;
@@ -30,18 +31,19 @@ public class AdminCommentController {
* 获取评论列表 - 管理接口
*/
@GetMapping("/list")
public Result<Page<CommentVO>> getCommentList(
public Result<Page<AdminCommentVO>> getCommentList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "videoId", required = false) Long videoId,
@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "keyword", required = false) String keyword,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.getAdminCommentList(page, size, videoId, userId, keyword);
return commentService.getAdminCommentList(page, size, videoId, userId, status, keyword);
}
/**
@@ -66,6 +68,17 @@ public class AdminCommentController {
return commentService.adminDeleteComment(commentId);
}
/**
* 恢复评论 - 管理接口
*/
@PutMapping("/{commentId}/restore")
public Result<Void> restoreComment(@PathVariable Long commentId, HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.adminRestoreComment(commentId);
}
/**
* 获取评论统计信息 - 管理接口
*/
@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 用户服务Feign客户端
@@ -35,5 +36,7 @@ public interface UserFeignClient {
@PostMapping("/user/notification/send/{userId}/{type}")
Result<Void> sendNotification(@PathVariable("userId") Long userId,
@PathVariable("type") Integer type,
@RequestParam(value = "content", required = false) String content,
@RequestParam(value = "relatedId", required = false) Long relatedId,
@RequestHeader("X-Internal-Token") String internalToken);
}
@@ -23,7 +23,7 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
}
@Override
public Result<Void> sendNotification(Long userId, Integer type, String internalToken) {
public Result<Void> sendNotification(Long userId, Integer type, String content, Long relatedId, String internalToken) {
log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type);
return Result.error("发送用户通知失败");
}
@@ -7,6 +7,7 @@ import com.nuaa.video.comment.dto.CommentQueryDTO;
import com.nuaa.video.comment.entity.Comment;
import com.nuaa.video.comment.vo.CommentStatVO;
import com.nuaa.video.comment.vo.CommentVO;
import com.nuaa.video.comment.vo.AdminCommentVO;
import com.nuaa.video.common.model.vo.Result;
/**
@@ -99,7 +100,7 @@ public interface CommentService extends IService<Comment> {
/**
* 获取评论列表 - 管理接口
*/
Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword);
Result<Page<AdminCommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword);
/**
* 获取评论详情 - 管理接口
@@ -111,8 +112,13 @@ public interface CommentService extends IService<Comment> {
*/
Result<Void> adminDeleteComment(Long commentId);
/**
* 管理员恢复评论 - 管理接口
*/
Result<Void> adminRestoreComment(Long commentId);
/**
* 获取评论统计信息 - 管理接口
*/
Result<CommentStatVO> getCommentStats();
}
}
@@ -277,10 +277,10 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
CompletableFuture.runAsync(() -> {
try {
if (!userId.equals(comment.getUserId())) {
userFeignClient.sendNotification(comment.getUserId(), 2, internalServiceToken);
userFeignClient.sendNotification(comment.getUserId(), 2, null, replyDTO.getVideoId(), internalServiceToken);
}
if (replyDTO.getReplyToId() != null && !userId.equals(replyDTO.getReplyToId())) {
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, internalServiceToken);
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, null, replyDTO.getVideoId(), internalServiceToken);
}
} catch (Exception e) {
log.warn("发送回复通知失败", e);
@@ -17,6 +17,7 @@ import com.nuaa.video.comment.service.CommentService;
import com.nuaa.video.comment.utils.SensitiveWordFilter;
import com.nuaa.video.comment.vo.CommentStatVO;
import com.nuaa.video.comment.vo.CommentVO;
import com.nuaa.video.comment.vo.AdminCommentVO;
import com.nuaa.video.comment.vo.ReplyVO;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
@@ -94,7 +95,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
updateVideoCommentCountAsync(commentDTO.getVideoId(), 1);
sendCommentNotificationAsync(videoResult.getData(), userId);
sendCommentNotificationAsync(videoResult.getData(), userId, commentDTO.getVideoId());
return Result.success(comment.getId());
}
@@ -361,7 +362,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
}
@Override
public Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword) {
public Result<Page<AdminCommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, Integer status, String keyword) {
if (page == null || page < 1) {
page = 1;
}
@@ -372,7 +373,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
// 构建查询条件
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
// 管理员可以查看所有状态的评论,包括已删除的
if (status != null) {
queryWrapper.eq(Comment::getStatus, status);
}
// 按视频ID筛选
if (videoId != null) {
@@ -397,12 +400,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
commentPage = page(commentPage, queryWrapper);
// 转换为VO
Page<CommentVO> voPage = new Page<>();
Page<AdminCommentVO> voPage = new Page<>();
BeanUtils.copyProperties(commentPage, voPage, "records");
List<CommentVO> commentVOList = new ArrayList<>();
List<AdminCommentVO> commentVOList = new ArrayList<>();
for (Comment comment : commentPage.getRecords()) {
CommentVO vo = convertToCommentVO(comment, null);
AdminCommentVO vo = convertToAdminCommentVO(comment);
commentVOList.add(vo);
}
@@ -453,6 +456,23 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> adminRestoreComment(Long commentId) {
Comment comment = getById(commentId);
if (comment == null) {
return Result.error("评论不存在");
}
if (comment.getStatus() == 1) {
return Result.success();
}
commentMapper.updateCommentStatus(commentId, 1, "admin");
updateVideoCommentCountAsync(comment.getVideoId(), 1);
return Result.success();
}
@Override
public Result<CommentStatVO> getCommentStats() {
CommentStatVO statVO = new CommentStatVO();
@@ -514,14 +534,44 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return vo;
}
private void sendCommentNotificationAsync(Object videoPayload, Long userId) {
private AdminCommentVO convertToAdminCommentVO(Comment comment) {
AdminCommentVO vo = new AdminCommentVO();
BeanUtils.copyProperties(comment, vo);
try {
Result<Object> userResult = userFeignClient.getUserProfile(comment.getUserId());
if (userResult.getCode() == 200 && userResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
vo.setUserNickname((String) userData.get("nickname"));
vo.setUserAvatar((String) userData.get("avatar"));
}
} catch (Exception e) {
log.error("管理端获取评论用户信息失败, userId={}", comment.getUserId(), e);
}
try {
Result<Object> videoResult = contentFeignClient.getVideoInfo(comment.getVideoId());
if (videoResult.getCode() == 200 && videoResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
vo.setVideoTitle((String) videoData.get("title"));
}
} catch (Exception e) {
log.error("管理端获取视频信息失败, videoId={}", comment.getVideoId(), e);
}
return vo;
}
private void sendCommentNotificationAsync(Object videoPayload, Long userId, Long videoId) {
CompletableFuture.runAsync(() -> {
try {
@SuppressWarnings("unchecked")
Map<String, Object> videoData = (Map<String, Object>) videoPayload;
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
if (!userId.equals(videoAuthorId)) {
userFeignClient.sendNotification(videoAuthorId, 2, internalServiceToken);
userFeignClient.sendNotification(videoAuthorId, 2, null, videoId, internalServiceToken);
}
} catch (Exception e) {
log.warn("发送评论通知失败", e);
@@ -0,0 +1,41 @@
package com.nuaa.video.comment.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理端评论VO
*/
@Data
public class AdminCommentVO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long videoId;
private String videoTitle;
private Long userId;
private String userNickname;
private String userAvatar;
private String content;
private Integer likeCount;
private Integer replyCount;
private Integer topStatus;
/**
* 状态(0:已删除,1:正常)
*/
private Integer status;
private LocalDateTime createTime;
}
@@ -0,0 +1,75 @@
package com.nuaa.video.comment.service;
import com.nuaa.video.comment.dto.ReplyAddDTO;
import com.nuaa.video.comment.entity.Comment;
import com.nuaa.video.comment.feign.UserFeignClient;
import com.nuaa.video.comment.service.impl.CommentReplyServiceImpl;
import com.nuaa.video.comment.service.impl.CommentServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Map;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
@DisplayName("通知发送测试")
class NotificationDispatchTest {
@Mock
private UserFeignClient userFeignClient;
@InjectMocks
private CommentServiceImpl commentService;
@InjectMocks
private CommentReplyServiceImpl commentReplyService;
@Test
@DisplayName("评论通知应携带视频ID作为 relatedId")
void shouldSendCommentNotificationWithVideoId() {
ReflectionTestUtils.setField(commentService, "internalServiceToken", "internal-token");
ReflectionTestUtils.invokeMethod(
commentService,
"sendCommentNotificationAsync",
Map.of("userId", 200L),
100L,
300L
);
verify(userFeignClient, timeout(1000)).sendNotification(200L, 2, null, 300L, "internal-token");
}
@Test
@DisplayName("回复通知应携带视频ID作为 relatedId")
void shouldSendReplyNotificationWithVideoId() {
ReflectionTestUtils.setField(commentReplyService, "internalServiceToken", "internal-token");
Comment comment = new Comment();
comment.setUserId(200L);
ReplyAddDTO replyDTO = new ReplyAddDTO();
replyDTO.setReplyToId(300L);
replyDTO.setVideoId(400L);
ReflectionTestUtils.invokeMethod(
commentReplyService,
"sendReplyNotificationAsync",
comment,
replyDTO,
100L
);
verify(userFeignClient, timeout(1000)).sendNotification(eq(200L), eq(2), isNull(), eq(400L), eq("internal-token"));
verify(userFeignClient, timeout(1000)).sendNotification(eq(300L), eq(2), isNull(), eq(400L), eq("internal-token"));
}
}