fix(backend): Restore service flow and admin management integration

This commit is contained in:
2026-04-18 18:20:58 +08:00
parent 92a139e470
commit 3adb088eca
83 changed files with 1696 additions and 703 deletions
@@ -2,8 +2,11 @@ package com.nuaa.video.comment.config;
import com.nuaa.video.comment.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -15,11 +18,15 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Value("${service.auth.token}")
private String internalServiceToken;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
@@ -29,13 +36,21 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/comment/public/**",
"/comment/reply/public/**",
"/actuator/**",
"/error"
).permitAll()
.requestMatchers("/comment/admin/**")
.access((authentication, context) ->
new AuthorizationDecision(isInternalRequest(context.getRequest().getHeader("X-Internal-Token"))))
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
private boolean isInternalRequest(String internalToken) {
return internalServiceToken.equals(internalToken);
}
}
@@ -6,8 +6,10 @@ import com.nuaa.video.comment.service.CommentService;
import com.nuaa.video.comment.vo.CommentStatVO;
import com.nuaa.video.comment.vo.CommentVO;
import com.nuaa.video.common.model.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
/**
@@ -19,6 +21,9 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
public class AdminCommentController {
@Value("${service.auth.token}")
private String internalServiceToken;
private final CommentService commentService;
/**
@@ -30,7 +35,11 @@ public class AdminCommentController {
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "videoId", required = false) Long videoId,
@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "keyword", required = false) String keyword) {
@RequestParam(value = "keyword", required = false) String keyword,
HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.getAdminCommentList(page, size, videoId, userId, keyword);
}
@@ -39,7 +48,10 @@ public class AdminCommentController {
* 获取评论详情 - 管理接口
*/
@GetMapping("/{commentId}")
public Result<CommentVO> getCommentDetail(@PathVariable Long commentId) {
public Result<CommentVO> getCommentDetail(@PathVariable Long commentId, HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.getAdminCommentDetail(commentId);
}
@@ -47,7 +59,10 @@ public class AdminCommentController {
* 删除评论 - 管理接口
*/
@DeleteMapping("/{commentId}")
public Result<Void> deleteComment(@PathVariable Long commentId) {
public Result<Void> deleteComment(@PathVariable Long commentId, HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.adminDeleteComment(commentId);
}
@@ -55,7 +70,14 @@ public class AdminCommentController {
* 获取评论统计信息 - 管理接口
*/
@GetMapping("/stats")
public Result<CommentStatVO> getCommentStats() {
public Result<CommentStatVO> getCommentStats(HttpServletRequest request) {
if (!isInternalRequest(request)) {
return Result.error("非法的内部调用");
}
return commentService.getCommentStats();
}
}
private boolean isInternalRequest(HttpServletRequest request) {
return internalServiceToken.equals(request.getHeader("X-Internal-Token"));
}
}
@@ -19,7 +19,7 @@ public interface ContentFeignClient {
* @param id 视频ID
* @return 视频信息
*/
@GetMapping("/content/video/{id}")
@GetMapping("/content/video/public/{id}")
Result<Object> getVideoInfo(@PathVariable("id") Long id);
/**
@@ -33,5 +33,5 @@ public interface ContentFeignClient {
@PostMapping("/content/video/count/comment/{videoId}/{count}")
Result<Void> increaseCommentCount(@PathVariable("videoId") Long videoId,
@PathVariable("count") Integer count,
@RequestHeader("Authorization") String token);
}
@RequestHeader("X-Internal-Token") String internalToken);
}
@@ -23,10 +23,10 @@ public class ContentFeignClientFallbackFactory implements FallbackFactory<Conten
}
@Override
public Result<Void> increaseCommentCount(Long videoId, Integer count, String token) {
public Result<Void> increaseCommentCount(Long videoId, Integer count, String internalToken) {
log.error("增加视频评论数量失败,视频ID:{}, 数量:{}", videoId, count);
return Result.error("更新视频评论数量失败");
}
};
}
}
}
@@ -35,5 +35,5 @@ public interface UserFeignClient {
@PostMapping("/user/notification/send/{userId}/{type}")
Result<Void> sendNotification(@PathVariable("userId") Long userId,
@PathVariable("type") Integer type,
@RequestHeader("Authorization") String token);
}
@RequestHeader("X-Internal-Token") String internalToken);
}
@@ -23,10 +23,10 @@ public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeign
}
@Override
public Result<Void> sendNotification(Long userId, Integer type, String token) {
public Result<Void> sendNotification(Long userId, Integer type, String internalToken) {
log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type);
return Result.error("发送用户通知失败");
}
};
}
}
}
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT认证过滤器
@@ -29,7 +30,7 @@ import java.util.List;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
@Value("${jwt.secret}")
private String secret;
private static final String TOKEN_PREFIX = "Bearer ";
@@ -55,7 +56,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String username = claims.get("username", String.class);
// 设置用户权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<String> authorityClaims = claims.get("authorities", List.class);
List<SimpleGrantedAuthority> authorities = authorityClaims == null
? new ArrayList<>()
: authorityClaims.stream()
.filter(StringUtils::hasText)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toCollection(ArrayList::new));
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
@@ -70,6 +77,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 在请求属性中存储用户ID
request.setAttribute("userId", userId);
request.setAttribute("username", username);
if (!authorities.isEmpty()) {
request.setAttribute("adminId", userId);
}
log.debug("认证成功: 用户ID={}, 用户名={}", userId, username);
} catch (Exception e) {
@@ -99,4 +109,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
}
@@ -26,6 +26,7 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* 评论回复服务实现类
@@ -35,6 +36,9 @@ import java.util.Map;
@RequiredArgsConstructor
public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, CommentReply> implements CommentReplyService {
@Value("${service.auth.token}")
private String internalServiceToken;
private final CommentReplyMapper commentReplyMapper;
private final CommentMapper commentMapper;
private final CommentLikeMapper commentLikeMapper;
@@ -80,20 +84,7 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
// 更新评论回复数
commentMapper.increaseReplyCount(replyDTO.getCommentId(), 1);
// 发送回复通知给评论作者
try {
// 如果回复者不是评论作者,发送通知
if (!userId.equals(comment.getUserId())) {
userFeignClient.sendNotification(comment.getUserId(), 2, "Bearer ");
}
// 如果存在回复目标用户,且回复者不是回复目标用户,发送通知
if (replyDTO.getReplyToId() != null && !userId.equals(replyDTO.getReplyToId())) {
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, "Bearer ");
}
} catch (Exception e) {
log.error("发送回复通知失败", e);
}
sendReplyNotificationAsync(comment, replyDTO, userId);
return Result.success(reply.getId());
}
@@ -161,6 +152,9 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
if (reply == null) {
return Result.error("回复不存在");
}
if (reply.getStatus() == 0) {
return Result.error("回复已删除");
}
// 校验操作权限
if (!reply.getUserId().equals(userId)) {
@@ -278,4 +272,19 @@ public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, Com
return vo;
}
}
private void sendReplyNotificationAsync(Comment comment, ReplyAddDTO replyDTO, Long userId) {
CompletableFuture.runAsync(() -> {
try {
if (!userId.equals(comment.getUserId())) {
userFeignClient.sendNotification(comment.getUserId(), 2, internalServiceToken);
}
if (replyDTO.getReplyToId() != null && !userId.equals(replyDTO.getReplyToId())) {
userFeignClient.sendNotification(replyDTO.getReplyToId(), 2, internalServiceToken);
}
} catch (Exception e) {
log.warn("发送回复通知失败", e);
}
});
}
}
@@ -32,6 +32,7 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* 评论服务实现类
@@ -41,6 +42,9 @@ import java.util.Map;
@RequiredArgsConstructor
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
@Value("${service.auth.token}")
private String internalServiceToken;
private final CommentMapper commentMapper;
private final CommentReplyMapper commentReplyMapper;
private final CommentLikeMapper commentLikeMapper;
@@ -88,22 +92,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
// 保存评论
save(comment);
// 更新视频评论数
contentFeignClient.increaseCommentCount(commentDTO.getVideoId(), 1, "Bearer ");
updateVideoCommentCountAsync(commentDTO.getVideoId(), 1);
// 发送评论通知给视频作者
try {
@SuppressWarnings("unchecked")
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
// 如果评论者不是视频作者,发送通知
if (!userId.equals(videoAuthorId)) {
userFeignClient.sendNotification(videoAuthorId, 2, "Bearer ");
}
} catch (Exception e) {
log.error("发送评论通知失败", e);
}
sendCommentNotificationAsync(videoResult.getData(), userId);
return Result.success(comment.getId());
}
@@ -176,6 +167,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment == null) {
return Result.error("评论不存在");
}
if (comment.getStatus() == 0) {
return Result.error("评论已删除");
}
// 校验操作权限
if (!comment.getUserId().equals(userId)) {
@@ -188,8 +182,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
comment.setUpdateTime(LocalDateTime.now());
updateById(comment);
// 更新视频评论数
contentFeignClient.increaseCommentCount(comment.getVideoId(), -1, "Bearer ");
updateVideoCommentCountAsync(comment.getVideoId(), -1);
return Result.success();
}
@@ -258,6 +251,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment == null || comment.getStatus() == 0) {
return Result.error("评论不存在");
}
if (!videoId.equals(comment.getVideoId())) {
return Result.error("评论不属于该视频");
}
// 验证视频所有权(只有视频作者才能置顶评论)
Result<Object> videoResult = contentFeignClient.getVideoInfo(videoId);
@@ -308,6 +304,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment == null || comment.getStatus() == 0) {
return Result.error("评论不存在");
}
if (!videoId.equals(comment.getVideoId())) {
return Result.error("评论不属于该视频");
}
// 验证视频所有权(只有视频作者才能取消置顶评论)
Result<Object> videoResult = contentFeignClient.getVideoInfo(videoId);
@@ -422,6 +421,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment == null) {
return Result.error("评论不存在");
}
if (comment.getStatus() == 0) {
return Result.error("评论已删除");
}
// 转换为VO
CommentVO vo = convertToCommentVO(comment, null);
@@ -452,8 +454,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
comment.setUpdateTime(LocalDateTime.now());
updateById(comment);
// 更新视频评论数
contentFeignClient.increaseCommentCount(comment.getVideoId(), -1, "Bearer ");
updateVideoCommentCountAsync(comment.getVideoId(), -1);
return Result.success();
}
@@ -464,16 +465,20 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
// 获取各种统计数据
// 1. 总评论数
long totalComments = count();
long totalComments = count(new LambdaQueryWrapper<Comment>().eq(Comment::getStatus, 1));
statVO.setCommentCount((int)totalComments);
// 2. 今日新增评论数
LocalDate today = LocalDate.now();
long todayComments = count(new LambdaQueryWrapper<Comment>()
.eq(Comment::getStatus, 1)
.ge(Comment::getCreateTime, today.atStartOfDay()));
// 3. 回复总数
long totalReplies = commentReplyMapper.selectCount(null);
long totalReplies = commentReplyMapper.selectCount(
new LambdaQueryWrapper<com.nuaa.video.comment.entity.CommentReply>()
.eq(com.nuaa.video.comment.entity.CommentReply::getStatus, 1)
);
statVO.setReplyCount((int)totalReplies);
// 4. 总点赞数
@@ -514,4 +519,32 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return vo;
}
}
private void sendCommentNotificationAsync(Object videoPayload, Long userId) {
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);
}
} catch (Exception e) {
log.warn("发送评论通知失败", e);
}
});
}
private void updateVideoCommentCountAsync(Long videoId, int delta) {
CompletableFuture.runAsync(() -> {
try {
Result<Void> countResult = contentFeignClient.increaseCommentCount(videoId, delta, internalServiceToken);
if (countResult == null || countResult.getCode() != 200) {
log.warn("异步更新视频评论数失败,videoId={}, delta={}, result={}", videoId, delta, countResult);
}
} catch (Exception e) {
log.warn("异步更新视频评论数异常,videoId={}, delta={}", videoId, delta, e);
}
});
}
}
@@ -1,5 +1,5 @@
server:
port: 9005
port: 9004
spring:
application:
@@ -18,14 +18,14 @@ spring:
datasource:
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/video_comment?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
password: ${MYSQL_ROOT_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
password: redis_EsJ8FX
database: 4
password: ${REDIS_PASSWORD}
database: 3
timeout: 10000
lettuce:
pool:
@@ -40,10 +40,19 @@ mybatis-plus:
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: is_deleted
logic-delete-value: 1
logic-not-delete-value: 0
logic-not-delete-value: 0
service:
auth:
token: ${SERVICE_AUTH_TOKEN}
jwt:
secret: ${USER_JWT_SECRET}
expiration: ${JWT_EXPIRATION}
token-header: Authorization
token-prefix: Bearer
@@ -17,7 +17,9 @@ 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.MockitoSettings;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -31,6 +33,7 @@ import static org.mockito.Mockito.*;
* CommentService 单元测试
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@DisplayName("评论服务测试")
class CommentServiceTest {