refactor(project): Reorganize frontend and backend directories
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.nuaa.video.comment;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* 评论服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients
|
||||
@MapperScan("com.nuaa.video.comment.mapper")
|
||||
public class CommentApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CommentApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.nuaa.video.comment.config;
|
||||
|
||||
import com.nuaa.video.comment.filter.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* 安全配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/comment/public/**",
|
||||
"/actuator/**",
|
||||
"/error"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
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.CommentStatVO;
|
||||
import com.nuaa.video.comment.vo.CommentVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 评论管理控制器 - 供管理后台调用
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/comment/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminCommentController {
|
||||
|
||||
private final CommentService commentService;
|
||||
|
||||
/**
|
||||
* 获取评论列表 - 管理接口
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<Page<CommentVO>> 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 = "keyword", required = false) String keyword) {
|
||||
|
||||
return commentService.getAdminCommentList(page, size, videoId, userId, keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论详情 - 管理接口
|
||||
*/
|
||||
@GetMapping("/{commentId}")
|
||||
public Result<CommentVO> getCommentDetail(@PathVariable Long commentId) {
|
||||
return commentService.getAdminCommentDetail(commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论 - 管理接口
|
||||
*/
|
||||
@DeleteMapping("/{commentId}")
|
||||
public Result<Void> deleteComment(@PathVariable Long commentId) {
|
||||
return commentService.adminDeleteComment(commentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论统计信息 - 管理接口
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public Result<CommentStatVO> getCommentStats() {
|
||||
return commentService.getCommentStats();
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.nuaa.video.comment.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.dto.CommentAddDTO;
|
||||
import com.nuaa.video.comment.dto.CommentQueryDTO;
|
||||
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 jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 评论控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/comment")
|
||||
@RequiredArgsConstructor
|
||||
public class CommentController {
|
||||
|
||||
private final CommentService commentService;
|
||||
|
||||
/**
|
||||
* 添加评论
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Long> addComment(@RequestBody @Valid CommentAddDTO commentDTO, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.addComment(commentDTO, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论列表(公开接口)
|
||||
*/
|
||||
@GetMapping("/public/list")
|
||||
public Result<Page<CommentVO>> getCommentList(CommentQueryDTO queryDTO, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
return commentService.getCommentList(queryDTO, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论详情(公开接口)
|
||||
*/
|
||||
@GetMapping("/public/detail/{id}")
|
||||
public Result<CommentVO> getCommentDetail(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
return commentService.getCommentDetail(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteComment(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.deleteComment(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞评论
|
||||
*/
|
||||
@PostMapping("/like/{id}")
|
||||
public Result<Void> likeComment(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.likeComment(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消点赞评论
|
||||
*/
|
||||
@DeleteMapping("/like/{id}")
|
||||
public Result<Void> unlikeComment(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.unlikeComment(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 置顶评论
|
||||
*/
|
||||
@PostMapping("/top/{id}")
|
||||
public Result<Void> topComment(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long videoId,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.topComment(id, userId, videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消置顶评论
|
||||
*/
|
||||
@DeleteMapping("/top/{id}")
|
||||
public Result<Void> untopComment(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Long videoId,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentService.untopComment(id, userId, videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论统计信息(公开接口)
|
||||
*/
|
||||
@GetMapping("/public/stat/{videoId}")
|
||||
public Result<CommentStatVO> getCommentStat(@PathVariable Long videoId) {
|
||||
return commentService.getCommentStat(videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户ID
|
||||
*/
|
||||
private Long getUserIdFromRequest(HttpServletRequest request) {
|
||||
Object userIdObj = request.getAttribute("userId");
|
||||
if (userIdObj instanceof Long) {
|
||||
return (Long) userIdObj;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.nuaa.video.comment.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.dto.ReplyAddDTO;
|
||||
import com.nuaa.video.comment.service.CommentReplyService;
|
||||
import com.nuaa.video.comment.vo.ReplyVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 回复控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/comment/reply")
|
||||
@RequiredArgsConstructor
|
||||
public class ReplyController {
|
||||
|
||||
private final CommentReplyService commentReplyService;
|
||||
|
||||
/**
|
||||
* 添加回复
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<Long> addReply(@RequestBody @Valid ReplyAddDTO replyDTO, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentReplyService.addReply(replyDTO, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论的回复列表(公开接口)
|
||||
*/
|
||||
@GetMapping("/public/list/{commentId}")
|
||||
public Result<Page<ReplyVO>> getReplyList(
|
||||
@PathVariable Long commentId,
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "10") Integer size,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
return commentReplyService.getReplyList(commentId, page, size, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论的热门回复(公开接口)
|
||||
*/
|
||||
@GetMapping("/public/hot/{commentId}")
|
||||
public Result<List<ReplyVO>> getHotReplies(
|
||||
@PathVariable Long commentId,
|
||||
@RequestParam(value = "limit", defaultValue = "3") Integer limit,
|
||||
HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
return commentReplyService.getHotReplies(commentId, limit, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除回复
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteReply(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentReplyService.deleteReply(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞回复
|
||||
*/
|
||||
@PostMapping("/like/{id}")
|
||||
public Result<Void> likeReply(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentReplyService.likeReply(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消点赞回复
|
||||
*/
|
||||
@DeleteMapping("/like/{id}")
|
||||
public Result<Void> unlikeReply(@PathVariable Long id, HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未登录");
|
||||
}
|
||||
return commentReplyService.unlikeReply(id, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户ID
|
||||
*/
|
||||
private Long getUserIdFromRequest(HttpServletRequest request) {
|
||||
Object userIdObj = request.getAttribute("userId");
|
||||
if (userIdObj instanceof Long) {
|
||||
return (Long) userIdObj;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.nuaa.video.comment.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 评论添加DTO
|
||||
*/
|
||||
@Data
|
||||
public class CommentAddDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
@NotNull(message = "视频ID不能为空")
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
@NotBlank(message = "评论内容不能为空")
|
||||
@Size(max = 500, message = "评论内容最多500字")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.nuaa.video.comment.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 评论查询DTO
|
||||
*/
|
||||
@Data
|
||||
public class CommentQueryDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String orderBy;
|
||||
|
||||
/**
|
||||
* 排序方向
|
||||
*/
|
||||
private String orderDirection;
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
private Integer pageNum = 1;
|
||||
|
||||
/**
|
||||
* 每页条数
|
||||
*/
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.nuaa.video.comment.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 回复添加DTO
|
||||
*/
|
||||
@Data
|
||||
public class ReplyAddDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
@NotNull(message = "评论ID不能为空")
|
||||
private Long commentId;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
@NotNull(message = "视频ID不能为空")
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 回复目标用户ID
|
||||
*/
|
||||
private Long replyToId;
|
||||
|
||||
/**
|
||||
* 回复内容
|
||||
*/
|
||||
@NotBlank(message = "回复内容不能为空")
|
||||
@Size(max = 500, message = "回复内容最多500字")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.nuaa.video.comment.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 评论实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("comment")
|
||||
public class Comment implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 回复数
|
||||
*/
|
||||
private Integer replyCount;
|
||||
|
||||
/**
|
||||
* 置顶状态(0:普通,1:置顶)
|
||||
*/
|
||||
private Integer topStatus;
|
||||
|
||||
/**
|
||||
* 状态(0:删除,1:正常)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 用户昵称(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String userNickname;
|
||||
|
||||
/**
|
||||
* 用户头像(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 是否已点赞(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Boolean isLiked;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.nuaa.video.comment.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 评论点赞实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("comment_like")
|
||||
public class CommentLike implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
private Long commentId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 类型(1:评论,2:回复)
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.nuaa.video.comment.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 评论回复实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("comment_reply")
|
||||
public class CommentReply implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 回复ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
private Long commentId;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 回复目标用户ID
|
||||
*/
|
||||
private Long replyToId;
|
||||
|
||||
/**
|
||||
* 回复内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 状态(0:删除,1:正常)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 用户昵称(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String userNickname;
|
||||
|
||||
/**
|
||||
* 用户头像(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 回复目标用户昵称(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String replyToNickname;
|
||||
|
||||
/**
|
||||
* 回复目标用户头像(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String replyToAvatar;
|
||||
|
||||
/**
|
||||
* 是否已点赞(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Boolean isLiked;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.nuaa.video.comment.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 内容服务Feign客户端
|
||||
*/
|
||||
@FeignClient(name = "video-content", fallbackFactory = ContentFeignClientFallbackFactory.class)
|
||||
public interface ContentFeignClient {
|
||||
|
||||
/**
|
||||
* 获取视频信息
|
||||
*
|
||||
* @param id 视频ID
|
||||
* @return 视频信息
|
||||
*/
|
||||
@GetMapping("/content/video/{id}")
|
||||
Result<Object> getVideoInfo(@PathVariable("id") Long id);
|
||||
|
||||
/**
|
||||
* 增加视频评论数
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @param count 增加数量
|
||||
* @param token 认证令牌
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/content/video/count/comment/{videoId}/{count}")
|
||||
Result<Void> increaseCommentCount(@PathVariable("videoId") Long videoId,
|
||||
@PathVariable("count") Integer count,
|
||||
@RequestHeader("Authorization") String token);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.nuaa.video.comment.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 内容服务Feign客户端降级工厂
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ContentFeignClientFallbackFactory implements FallbackFactory<ContentFeignClient> {
|
||||
|
||||
@Override
|
||||
public ContentFeignClient create(Throwable cause) {
|
||||
log.error("内容服务调用失败:", cause);
|
||||
return new ContentFeignClient() {
|
||||
@Override
|
||||
public Result<Object> getVideoInfo(Long id) {
|
||||
log.error("获取视频信息失败,视频ID:{}", id);
|
||||
return Result.error("获取视频信息失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> increaseCommentCount(Long videoId, Integer count, String token) {
|
||||
log.error("增加视频评论数量失败,视频ID:{}, 数量:{}", videoId, count);
|
||||
return Result.error("更新视频评论数量失败");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.nuaa.video.comment.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端
|
||||
*/
|
||||
@FeignClient(name = "video-user", fallbackFactory = UserFeignClientFallbackFactory.class)
|
||||
public interface UserFeignClient {
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
@GetMapping("/user/profile/public/{userId}")
|
||||
Result<Object> getUserProfile(@PathVariable("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 发送用户通知
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param type 通知类型
|
||||
* @param content 通知内容
|
||||
* @param relatedId 相关ID
|
||||
* @param token 认证令牌
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/user/notification/send/{userId}/{type}")
|
||||
Result<Void> sendNotification(@PathVariable("userId") Long userId,
|
||||
@PathVariable("type") Integer type,
|
||||
@RequestHeader("Authorization") String token);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.nuaa.video.comment.feign;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 用户服务Feign客户端降级工厂
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
|
||||
|
||||
@Override
|
||||
public UserFeignClient create(Throwable cause) {
|
||||
log.error("用户服务调用失败:", cause);
|
||||
return new UserFeignClient() {
|
||||
@Override
|
||||
public Result<Object> getUserProfile(Long userId) {
|
||||
log.error("获取用户信息失败,用户ID:{}", userId);
|
||||
return Result.error("获取用户信息失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> sendNotification(Long userId, Integer type, String token) {
|
||||
log.error("发送用户通知失败,用户ID:{}, 类型:{}", userId, type);
|
||||
return Result.error("发送用户通知失败");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.nuaa.video.comment.filter;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
|
||||
private String secret;
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 从请求头中获取JWT令牌
|
||||
String token = getJwtFromRequest(request);
|
||||
|
||||
// 如果令牌存在,则本地验证
|
||||
if (StringUtils.hasText(token)) {
|
||||
try {
|
||||
// 验证令牌
|
||||
Claims claims = Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
|
||||
// 从令牌中提取用户ID和用户名
|
||||
Long userId = Long.parseLong(claims.getSubject());
|
||||
String username = claims.get("username", String.class);
|
||||
|
||||
// 设置用户权限
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(username, null, authorities);
|
||||
|
||||
// 设置额外信息
|
||||
authentication.setDetails(userId);
|
||||
|
||||
// 设置认证信息到安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 在请求属性中存储用户ID
|
||||
request.setAttribute("userId", userId);
|
||||
request.setAttribute("username", username);
|
||||
|
||||
log.debug("认证成功: 用户ID={}, 用户名={}", userId, username);
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败: {}", e.getMessage());
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.nuaa.video.comment.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.comment.entity.CommentLike;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 评论点赞Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CommentLikeMapper extends BaseMapper<CommentLike> {
|
||||
|
||||
/**
|
||||
* 检查用户是否已点赞
|
||||
*
|
||||
* @param commentId 评论/回复ID
|
||||
* @param userId 用户ID
|
||||
* @param type 类型(1:评论,2:回复)
|
||||
* @return 点赞记录数
|
||||
*/
|
||||
long checkUserLiked(@Param("commentId") Long commentId, @Param("userId") Long userId, @Param("type") Integer type);
|
||||
|
||||
/**
|
||||
* 统计视频评论点赞数
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 点赞数
|
||||
*/
|
||||
int countVideoCommentLikes(@Param("videoId") Long videoId);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.nuaa.video.comment.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.dto.CommentQueryDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 评论Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CommentMapper extends BaseMapper<Comment> {
|
||||
|
||||
/**
|
||||
* 分页查询评论列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param query 查询条件
|
||||
* @return 评论列表
|
||||
*/
|
||||
Page<Comment> selectCommentList(Page<Comment> page, @Param("query") CommentQueryDTO query);
|
||||
|
||||
/**
|
||||
* 增加点赞数
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
* @param count 增加数量
|
||||
* @return 影响行数
|
||||
*/
|
||||
int increaseLikeCount(@Param("commentId") Long commentId, @Param("count") int count);
|
||||
|
||||
/**
|
||||
* 增加回复数
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
* @param count 增加数量
|
||||
* @return 影响行数
|
||||
*/
|
||||
int increaseReplyCount(@Param("commentId") Long commentId, @Param("count") int count);
|
||||
|
||||
/**
|
||||
* 统计视频评论数
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 评论数
|
||||
*/
|
||||
int countVideoComments(@Param("videoId") Long videoId);
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.nuaa.video.comment.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.entity.CommentReply;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 评论回复Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface CommentReplyMapper extends BaseMapper<CommentReply> {
|
||||
|
||||
/**
|
||||
* 查询评论的回复列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param commentId 评论ID
|
||||
* @return 回复列表
|
||||
*/
|
||||
Page<CommentReply> selectCommentReplyList(Page<CommentReply> page, @Param("commentId") Long commentId);
|
||||
|
||||
/**
|
||||
* 获取评论的热门回复(前N条)
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
* @param limit 数量限制
|
||||
* @return 热门回复列表
|
||||
*/
|
||||
List<CommentReply> selectHotReplies(@Param("commentId") Long commentId, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 增加点赞数
|
||||
*
|
||||
* @param replyId 回复ID
|
||||
* @param count 增加数量
|
||||
* @return 影响行数
|
||||
*/
|
||||
int increaseLikeCount(@Param("replyId") Long replyId, @Param("count") int count);
|
||||
|
||||
/**
|
||||
* 统计视频回复数
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 回复数
|
||||
*/
|
||||
int countVideoReplies(@Param("videoId") Long videoId);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.nuaa.video.comment.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.nuaa.video.comment.dto.ReplyAddDTO;
|
||||
import com.nuaa.video.comment.entity.CommentReply;
|
||||
import com.nuaa.video.comment.vo.ReplyVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 评论回复服务接口
|
||||
*/
|
||||
public interface CommentReplyService extends IService<CommentReply> {
|
||||
|
||||
/**
|
||||
* 添加回复
|
||||
*
|
||||
* @param replyDTO 回复信息
|
||||
* @param userId 用户ID
|
||||
* @return 回复ID
|
||||
*/
|
||||
Result<Long> addReply(ReplyAddDTO replyDTO, Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询评论的回复列表
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @param userId 当前用户ID
|
||||
* @return 回复列表
|
||||
*/
|
||||
Result<Page<ReplyVO>> getReplyList(Long commentId, Integer page, Integer size, Long userId);
|
||||
|
||||
/**
|
||||
* 获取评论的热门回复
|
||||
*
|
||||
* @param commentId 评论ID
|
||||
* @param limit 数量限制
|
||||
* @param userId 当前用户ID
|
||||
* @return 热门回复列表
|
||||
*/
|
||||
Result<List<ReplyVO>> getHotReplies(Long commentId, Integer limit, Long userId);
|
||||
|
||||
/**
|
||||
* 删除回复
|
||||
*
|
||||
* @param id 回复ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> deleteReply(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 点赞回复
|
||||
*
|
||||
* @param id 回复ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> likeReply(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 取消点赞回复
|
||||
*
|
||||
* @param id 回复ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> unlikeReply(Long id, Long userId);
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.nuaa.video.comment.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.nuaa.video.comment.dto.CommentAddDTO;
|
||||
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.common.model.vo.Result;
|
||||
|
||||
/**
|
||||
* 评论服务接口
|
||||
*/
|
||||
public interface CommentService extends IService<Comment> {
|
||||
|
||||
/**
|
||||
* 添加评论
|
||||
*
|
||||
* @param commentDTO 评论信息
|
||||
* @param userId 用户ID
|
||||
* @return 评论ID
|
||||
*/
|
||||
Result<Long> addComment(CommentAddDTO commentDTO, Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询评论列表
|
||||
*
|
||||
* @param queryDTO 查询条件
|
||||
* @param userId 当前用户ID
|
||||
* @return 评论列表
|
||||
*/
|
||||
Result<Page<CommentVO>> getCommentList(CommentQueryDTO queryDTO, Long userId);
|
||||
|
||||
/**
|
||||
* 获取评论详情
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 当前用户ID
|
||||
* @return 评论详情
|
||||
*/
|
||||
Result<CommentVO> getCommentDetail(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> deleteComment(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 点赞评论
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> likeComment(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 取消点赞评论
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 用户ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> unlikeComment(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 置顶评论
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 用户ID
|
||||
* @param videoId 视频ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> topComment(Long id, Long userId, Long videoId);
|
||||
|
||||
/**
|
||||
* 取消置顶评论
|
||||
*
|
||||
* @param id 评论ID
|
||||
* @param userId 用户ID
|
||||
* @param videoId 视频ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
Result<Void> untopComment(Long id, Long userId, Long videoId);
|
||||
|
||||
/**
|
||||
* 获取评论统计信息
|
||||
*
|
||||
* @param videoId 视频ID
|
||||
* @return 统计信息
|
||||
*/
|
||||
Result<CommentStatVO> getCommentStat(Long videoId);
|
||||
|
||||
/**
|
||||
* 获取评论列表 - 管理接口
|
||||
*/
|
||||
Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword);
|
||||
|
||||
/**
|
||||
* 获取评论详情 - 管理接口
|
||||
*/
|
||||
Result<CommentVO> getAdminCommentDetail(Long commentId);
|
||||
|
||||
/**
|
||||
* 管理员删除评论 - 管理接口
|
||||
*/
|
||||
Result<Void> adminDeleteComment(Long commentId);
|
||||
|
||||
/**
|
||||
* 获取评论统计信息 - 管理接口
|
||||
*/
|
||||
Result<CommentStatVO> getCommentStats();
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
package com.nuaa.video.comment.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.nuaa.video.comment.dto.ReplyAddDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.entity.CommentLike;
|
||||
import com.nuaa.video.comment.entity.CommentReply;
|
||||
import com.nuaa.video.comment.feign.UserFeignClient;
|
||||
import com.nuaa.video.comment.mapper.CommentLikeMapper;
|
||||
import com.nuaa.video.comment.mapper.CommentMapper;
|
||||
import com.nuaa.video.comment.mapper.CommentReplyMapper;
|
||||
import com.nuaa.video.comment.service.CommentReplyService;
|
||||
import com.nuaa.video.comment.utils.SensitiveWordFilter;
|
||||
import com.nuaa.video.comment.vo.ReplyVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 评论回复服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CommentReplyServiceImpl extends ServiceImpl<CommentReplyMapper, CommentReply> implements CommentReplyService {
|
||||
|
||||
private final CommentReplyMapper commentReplyMapper;
|
||||
private final CommentMapper commentMapper;
|
||||
private final CommentLikeMapper commentLikeMapper;
|
||||
private final UserFeignClient userFeignClient;
|
||||
private final SensitiveWordFilter sensitiveWordFilter;
|
||||
|
||||
@Value("${comment.sensitive-filter.enabled:true}")
|
||||
private boolean sensitiveFilterEnabled;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Long> addReply(ReplyAddDTO replyDTO, Long userId) {
|
||||
// 校验评论是否存在
|
||||
Comment comment = commentMapper.selectById(replyDTO.getCommentId());
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 校验回复内容
|
||||
String content = replyDTO.getContent();
|
||||
if (sensitiveFilterEnabled && sensitiveWordFilter.containsSensitiveWord(content)) {
|
||||
// 包含敏感词,进行替换
|
||||
content = sensitiveWordFilter.replaceSensitiveWords(content, "*");
|
||||
}
|
||||
|
||||
// 创建回复对象
|
||||
CommentReply reply = new CommentReply();
|
||||
reply.setCommentId(replyDTO.getCommentId());
|
||||
reply.setVideoId(replyDTO.getVideoId());
|
||||
reply.setUserId(userId);
|
||||
reply.setReplyToId(replyDTO.getReplyToId());
|
||||
reply.setContent(content);
|
||||
reply.setLikeCount(0);
|
||||
reply.setStatus(1); // 默认正常状态
|
||||
reply.setCreatedBy(userId.toString());
|
||||
reply.setUpdatedBy(userId.toString());
|
||||
reply.setCreateTime(LocalDateTime.now());
|
||||
reply.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
// 保存回复
|
||||
save(reply);
|
||||
|
||||
// 更新评论回复数
|
||||
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);
|
||||
}
|
||||
|
||||
return Result.success(reply.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<ReplyVO>> getReplyList(Long commentId, Integer page, Integer size, Long userId) {
|
||||
// 参数校验
|
||||
if (commentId == null) {
|
||||
return Result.error("评论ID不能为空");
|
||||
}
|
||||
if (page == null || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
// 分页查询回复
|
||||
Page<CommentReply> replyPage = new Page<>(page, size);
|
||||
replyPage = commentReplyMapper.selectCommentReplyList(replyPage, commentId);
|
||||
|
||||
// 构建返回结果
|
||||
Page<ReplyVO> resultPage = new Page<>();
|
||||
BeanUtils.copyProperties(replyPage, resultPage, "records");
|
||||
|
||||
List<ReplyVO> replyVOList = new ArrayList<>();
|
||||
for (CommentReply reply : replyPage.getRecords()) {
|
||||
ReplyVO vo = convertToReplyVO(reply, userId);
|
||||
replyVOList.add(vo);
|
||||
}
|
||||
|
||||
resultPage.setRecords(replyVOList);
|
||||
|
||||
return Result.success(resultPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<ReplyVO>> getHotReplies(Long commentId, Integer limit, Long userId) {
|
||||
// 参数校验
|
||||
if (commentId == null) {
|
||||
return Result.error("评论ID不能为空");
|
||||
}
|
||||
if (limit == null || limit < 1) {
|
||||
limit = 3;
|
||||
}
|
||||
|
||||
// 查询热门回复
|
||||
List<CommentReply> hotReplies = commentReplyMapper.selectHotReplies(commentId, limit);
|
||||
|
||||
// 构建返回结果
|
||||
List<ReplyVO> replyVOList = new ArrayList<>();
|
||||
for (CommentReply reply : hotReplies) {
|
||||
ReplyVO vo = convertToReplyVO(reply, userId);
|
||||
replyVOList.add(vo);
|
||||
}
|
||||
|
||||
return Result.success(replyVOList);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteReply(Long id, Long userId) {
|
||||
// 查询回复
|
||||
CommentReply reply = getById(id);
|
||||
if (reply == null) {
|
||||
return Result.error("回复不存在");
|
||||
}
|
||||
|
||||
// 校验操作权限
|
||||
if (!reply.getUserId().equals(userId)) {
|
||||
return Result.error("无权删除该回复");
|
||||
}
|
||||
|
||||
// 逻辑删除回复(设置状态为0)
|
||||
reply.setStatus(0);
|
||||
reply.setUpdatedBy(userId.toString());
|
||||
reply.setUpdateTime(LocalDateTime.now());
|
||||
updateById(reply);
|
||||
|
||||
// 更新评论回复数
|
||||
commentMapper.increaseReplyCount(reply.getCommentId(), -1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> likeReply(Long id, Long userId) {
|
||||
// 查询回复
|
||||
CommentReply reply = getById(id);
|
||||
if (reply == null || reply.getStatus() == 0) {
|
||||
return Result.error("回复不存在");
|
||||
}
|
||||
|
||||
// 检查是否已点赞
|
||||
long count = commentLikeMapper.checkUserLiked(id, userId, 2);
|
||||
if (count > 0) {
|
||||
return Result.error("已经点赞过该回复");
|
||||
}
|
||||
|
||||
// 添加点赞记录
|
||||
CommentLike like = new CommentLike();
|
||||
like.setCommentId(id);
|
||||
like.setUserId(userId);
|
||||
like.setType(2); // 2-回复
|
||||
like.setCreateTime(LocalDateTime.now());
|
||||
commentLikeMapper.insert(like);
|
||||
|
||||
// 更新回复点赞数
|
||||
commentReplyMapper.increaseLikeCount(id, 1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> unlikeReply(Long id, Long userId) {
|
||||
// 查询回复
|
||||
CommentReply reply = getById(id);
|
||||
if (reply == null || reply.getStatus() == 0) {
|
||||
return Result.error("回复不存在");
|
||||
}
|
||||
|
||||
// 检查是否已点赞
|
||||
long count = commentLikeMapper.checkUserLiked(id, userId, 2);
|
||||
if (count == 0) {
|
||||
return Result.error("未点赞该回复");
|
||||
}
|
||||
|
||||
// 删除点赞记录
|
||||
commentLikeMapper.delete(new LambdaQueryWrapper<CommentLike>()
|
||||
.eq(CommentLike::getCommentId, id)
|
||||
.eq(CommentLike::getUserId, userId)
|
||||
.eq(CommentLike::getType, 2));
|
||||
|
||||
// 更新回复点赞数
|
||||
commentReplyMapper.increaseLikeCount(id, -1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将回复实体转换为VO
|
||||
*/
|
||||
private ReplyVO convertToReplyVO(CommentReply reply, Long userId) {
|
||||
ReplyVO vo = new ReplyVO();
|
||||
BeanUtils.copyProperties(reply, vo);
|
||||
|
||||
// 获取用户信息
|
||||
try {
|
||||
Result<Object> userResult = userFeignClient.getUserProfile(reply.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"));
|
||||
}
|
||||
|
||||
// 如果有回复目标用户,获取目标用户信息
|
||||
if (reply.getReplyToId() != null) {
|
||||
Result<Object> replyToResult = userFeignClient.getUserProfile(reply.getReplyToId());
|
||||
if (replyToResult.getCode() == 200 && replyToResult.getData() != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> replyToData = (Map<String, Object>) replyToResult.getData();
|
||||
|
||||
vo.setReplyToNickname((String) replyToData.get("nickname"));
|
||||
vo.setReplyToAvatar((String) replyToData.get("avatar"));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户信息失败, userId={}, replyToId={}", reply.getUserId(), reply.getReplyToId(), e);
|
||||
}
|
||||
|
||||
// 获取点赞状态
|
||||
if (userId != null) {
|
||||
long likeCount = commentLikeMapper.checkUserLiked(reply.getId(), userId, 2);
|
||||
vo.setIsLiked(likeCount > 0);
|
||||
} else {
|
||||
vo.setIsLiked(false);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
package com.nuaa.video.comment.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.nuaa.video.comment.dto.CommentAddDTO;
|
||||
import com.nuaa.video.comment.dto.CommentQueryDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.entity.CommentLike;
|
||||
import com.nuaa.video.comment.feign.ContentFeignClient;
|
||||
import com.nuaa.video.comment.feign.UserFeignClient;
|
||||
import com.nuaa.video.comment.mapper.CommentLikeMapper;
|
||||
import com.nuaa.video.comment.mapper.CommentMapper;
|
||||
import com.nuaa.video.comment.mapper.CommentReplyMapper;
|
||||
import com.nuaa.video.comment.service.CommentReplyService;
|
||||
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.ReplyVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 评论服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
|
||||
|
||||
private final CommentMapper commentMapper;
|
||||
private final CommentReplyMapper commentReplyMapper;
|
||||
private final CommentLikeMapper commentLikeMapper;
|
||||
private final CommentReplyService commentReplyService;
|
||||
private final ContentFeignClient contentFeignClient;
|
||||
private final UserFeignClient userFeignClient;
|
||||
private final SensitiveWordFilter sensitiveWordFilter;
|
||||
|
||||
@Value("${comment.sensitive-filter.enabled:true}")
|
||||
private boolean sensitiveFilterEnabled;
|
||||
|
||||
@Value("${comment.review.enabled:false}")
|
||||
private boolean reviewEnabled;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Long> addComment(CommentAddDTO commentDTO, Long userId) {
|
||||
// 校验视频是否存在
|
||||
Result<Object> videoResult = contentFeignClient.getVideoInfo(commentDTO.getVideoId());
|
||||
if (videoResult.getCode() != 200 || videoResult.getData() == null) {
|
||||
return Result.error("视频不存在");
|
||||
}
|
||||
|
||||
// 校验评论内容
|
||||
String content = commentDTO.getContent();
|
||||
if (sensitiveFilterEnabled && sensitiveWordFilter.containsSensitiveWord(content)) {
|
||||
// 包含敏感词,进行替换
|
||||
content = sensitiveWordFilter.replaceSensitiveWords(content, "*");
|
||||
}
|
||||
|
||||
// 创建评论对象
|
||||
Comment comment = new Comment();
|
||||
comment.setVideoId(commentDTO.getVideoId());
|
||||
comment.setUserId(userId);
|
||||
comment.setContent(content);
|
||||
comment.setLikeCount(0);
|
||||
comment.setReplyCount(0);
|
||||
comment.setTopStatus(0);
|
||||
comment.setStatus(1); // 默认正常状态
|
||||
comment.setCreatedBy(userId.toString());
|
||||
comment.setUpdatedBy(userId.toString());
|
||||
comment.setCreateTime(LocalDateTime.now());
|
||||
comment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
// 保存评论
|
||||
save(comment);
|
||||
|
||||
// 更新视频评论数
|
||||
contentFeignClient.increaseCommentCount(commentDTO.getVideoId(), 1, "Bearer ");
|
||||
|
||||
// 发送评论通知给视频作者
|
||||
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);
|
||||
}
|
||||
|
||||
return Result.success(comment.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<CommentVO>> getCommentList(CommentQueryDTO queryDTO, Long userId) {
|
||||
// 参数校验
|
||||
if (queryDTO.getPageNum() == null || queryDTO.getPageNum() < 1) {
|
||||
queryDTO.setPageNum(1);
|
||||
}
|
||||
if (queryDTO.getPageSize() == null || queryDTO.getPageSize() < 1) {
|
||||
queryDTO.setPageSize(10);
|
||||
}
|
||||
|
||||
// 分页查询评论
|
||||
Page<Comment> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
|
||||
page = commentMapper.selectCommentList(page, queryDTO);
|
||||
|
||||
// 构建返回结果
|
||||
Page<CommentVO> resultPage = new Page<>();
|
||||
BeanUtils.copyProperties(page, resultPage, "records");
|
||||
|
||||
List<CommentVO> commentVOList = new ArrayList<>();
|
||||
for (Comment comment : page.getRecords()) {
|
||||
CommentVO vo = convertToCommentVO(comment, userId);
|
||||
|
||||
// 获取热门回复
|
||||
Result<List<ReplyVO>> hotRepliesResult = commentReplyService.getHotReplies(comment.getId(), 3, userId);
|
||||
if (hotRepliesResult.getCode() == 200 && hotRepliesResult.getData() != null) {
|
||||
vo.setHotReplies(hotRepliesResult.getData());
|
||||
} else {
|
||||
vo.setHotReplies(new ArrayList<>());
|
||||
}
|
||||
|
||||
commentVOList.add(vo);
|
||||
}
|
||||
|
||||
resultPage.setRecords(commentVOList);
|
||||
|
||||
return Result.success(resultPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<CommentVO> getCommentDetail(Long id, Long userId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 转换为VO
|
||||
CommentVO commentVO = convertToCommentVO(comment, userId);
|
||||
|
||||
// 获取热门回复
|
||||
Result<List<ReplyVO>> hotRepliesResult = commentReplyService.getHotReplies(comment.getId(), 3, userId);
|
||||
if (hotRepliesResult.getCode() == 200 && hotRepliesResult.getData() != null) {
|
||||
commentVO.setHotReplies(hotRepliesResult.getData());
|
||||
} else {
|
||||
commentVO.setHotReplies(new ArrayList<>());
|
||||
}
|
||||
|
||||
return Result.success(commentVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteComment(Long id, Long userId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 校验操作权限
|
||||
if (!comment.getUserId().equals(userId)) {
|
||||
return Result.error("无权删除该评论");
|
||||
}
|
||||
|
||||
// 逻辑删除评论(设置状态为0)
|
||||
comment.setStatus(0);
|
||||
comment.setUpdatedBy(userId.toString());
|
||||
comment.setUpdateTime(LocalDateTime.now());
|
||||
updateById(comment);
|
||||
|
||||
// 更新视频评论数
|
||||
contentFeignClient.increaseCommentCount(comment.getVideoId(), -1, "Bearer ");
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> likeComment(Long id, Long userId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 检查是否已点赞
|
||||
long count = commentLikeMapper.checkUserLiked(id, userId, 1);
|
||||
if (count > 0) {
|
||||
return Result.error("已经点赞过该评论");
|
||||
}
|
||||
|
||||
// 添加点赞记录
|
||||
CommentLike like = new CommentLike();
|
||||
like.setCommentId(id);
|
||||
like.setUserId(userId);
|
||||
like.setType(1); // 1-评论
|
||||
like.setCreateTime(LocalDateTime.now());
|
||||
commentLikeMapper.insert(like);
|
||||
|
||||
// 更新评论点赞数
|
||||
commentMapper.increaseLikeCount(id, 1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> unlikeComment(Long id, Long userId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 检查是否已点赞
|
||||
long count = commentLikeMapper.checkUserLiked(id, userId, 1);
|
||||
if (count == 0) {
|
||||
return Result.error("未点赞该评论");
|
||||
}
|
||||
|
||||
// 删除点赞记录
|
||||
commentLikeMapper.delete(new LambdaQueryWrapper<CommentLike>()
|
||||
.eq(CommentLike::getCommentId, id)
|
||||
.eq(CommentLike::getUserId, userId)
|
||||
.eq(CommentLike::getType, 1));
|
||||
|
||||
// 更新评论点赞数
|
||||
commentMapper.increaseLikeCount(id, -1);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> topComment(Long id, Long userId, Long videoId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 验证视频所有权(只有视频作者才能置顶评论)
|
||||
Result<Object> videoResult = contentFeignClient.getVideoInfo(videoId);
|
||||
if (videoResult.getCode() != 200 || videoResult.getData() == null) {
|
||||
return Result.error("视频不存在");
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
|
||||
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
|
||||
|
||||
if (!userId.equals(videoAuthorId)) {
|
||||
return Result.error("只有视频作者才能置顶评论");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析视频信息失败", e);
|
||||
return Result.error("获取视频信息失败");
|
||||
}
|
||||
|
||||
// 取消之前的置顶评论
|
||||
LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<Comment>()
|
||||
.eq(Comment::getVideoId, videoId)
|
||||
.eq(Comment::getTopStatus, 1);
|
||||
|
||||
List<Comment> topComments = list(wrapper);
|
||||
for (Comment topComment : topComments) {
|
||||
topComment.setTopStatus(0);
|
||||
topComment.setUpdatedBy(userId.toString());
|
||||
topComment.setUpdateTime(LocalDateTime.now());
|
||||
updateById(topComment);
|
||||
}
|
||||
|
||||
// 设置新的置顶评论
|
||||
comment.setTopStatus(1);
|
||||
comment.setUpdatedBy(userId.toString());
|
||||
comment.setUpdateTime(LocalDateTime.now());
|
||||
updateById(comment);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> untopComment(Long id, Long userId, Long videoId) {
|
||||
// 查询评论
|
||||
Comment comment = getById(id);
|
||||
if (comment == null || comment.getStatus() == 0) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 验证视频所有权(只有视频作者才能取消置顶评论)
|
||||
Result<Object> videoResult = contentFeignClient.getVideoInfo(videoId);
|
||||
if (videoResult.getCode() != 200 || videoResult.getData() == null) {
|
||||
return Result.error("视频不存在");
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> videoData = (Map<String, Object>) videoResult.getData();
|
||||
Long videoAuthorId = Long.valueOf(videoData.get("userId").toString());
|
||||
|
||||
if (!userId.equals(videoAuthorId)) {
|
||||
return Result.error("只有视频作者才能取消置顶评论");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析视频信息失败", e);
|
||||
return Result.error("获取视频信息失败");
|
||||
}
|
||||
|
||||
// 取消置顶评论
|
||||
if (comment.getTopStatus() == 1) {
|
||||
comment.setTopStatus(0);
|
||||
comment.setUpdatedBy(userId.toString());
|
||||
comment.setUpdateTime(LocalDateTime.now());
|
||||
updateById(comment);
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<CommentStatVO> getCommentStat(Long videoId) {
|
||||
if (videoId == null) {
|
||||
return Result.error("视频ID不能为空");
|
||||
}
|
||||
|
||||
// 统计评论数量
|
||||
int commentCount = commentMapper.countVideoComments(videoId);
|
||||
|
||||
// 统计回复数量
|
||||
int replyCount = commentReplyMapper.countVideoReplies(videoId);
|
||||
|
||||
// 统计点赞数量
|
||||
int likeCount = commentLikeMapper.countVideoCommentLikes(videoId);
|
||||
|
||||
// 构建统计结果
|
||||
CommentStatVO statVO = new CommentStatVO();
|
||||
statVO.setVideoId(videoId);
|
||||
statVO.setCommentCount(commentCount);
|
||||
statVO.setReplyCount(replyCount);
|
||||
statVO.setLikeCount(likeCount);
|
||||
|
||||
return Result.success(statVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword) {
|
||||
if (page == null || page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (size == null || size < 1) {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 管理员可以查看所有状态的评论,包括已删除的
|
||||
|
||||
// 按视频ID筛选
|
||||
if (videoId != null) {
|
||||
queryWrapper.eq(Comment::getVideoId, videoId);
|
||||
}
|
||||
|
||||
// 按用户ID筛选
|
||||
if (userId != null) {
|
||||
queryWrapper.eq(Comment::getUserId, userId);
|
||||
}
|
||||
|
||||
// 按关键词筛选
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
queryWrapper.like(Comment::getContent, keyword);
|
||||
}
|
||||
|
||||
// 按创建时间降序排序
|
||||
queryWrapper.orderByDesc(Comment::getCreateTime);
|
||||
|
||||
// 分页查询
|
||||
Page<Comment> commentPage = new Page<>(page, size);
|
||||
commentPage = page(commentPage, queryWrapper);
|
||||
|
||||
// 转换为VO
|
||||
Page<CommentVO> voPage = new Page<>();
|
||||
BeanUtils.copyProperties(commentPage, voPage, "records");
|
||||
|
||||
List<CommentVO> commentVOList = new ArrayList<>();
|
||||
for (Comment comment : commentPage.getRecords()) {
|
||||
CommentVO vo = convertToCommentVO(comment, null);
|
||||
commentVOList.add(vo);
|
||||
}
|
||||
|
||||
voPage.setRecords(commentVOList);
|
||||
|
||||
return Result.success(voPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<CommentVO> getAdminCommentDetail(Long commentId) {
|
||||
// 查询评论信息
|
||||
Comment comment = getById(commentId);
|
||||
if (comment == null) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 转换为VO
|
||||
CommentVO vo = convertToCommentVO(comment, null);
|
||||
|
||||
// 获取回复列表
|
||||
Result<List<ReplyVO>> repliesResult = commentReplyService.getHotReplies(commentId, 10, null);
|
||||
if (repliesResult.getCode() == 200 && repliesResult.getData() != null) {
|
||||
vo.setHotReplies(repliesResult.getData());
|
||||
} else {
|
||||
vo.setHotReplies(new ArrayList<>());
|
||||
}
|
||||
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> adminDeleteComment(Long commentId) {
|
||||
// 查询评论是否存在
|
||||
Comment comment = getById(commentId);
|
||||
if (comment == null) {
|
||||
return Result.error("评论不存在");
|
||||
}
|
||||
|
||||
// 逻辑删除评论(设置状态为0)
|
||||
comment.setStatus(0);
|
||||
comment.setUpdatedBy("admin");
|
||||
comment.setUpdateTime(LocalDateTime.now());
|
||||
updateById(comment);
|
||||
|
||||
// 更新视频评论数
|
||||
contentFeignClient.increaseCommentCount(comment.getVideoId(), -1, "Bearer ");
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<CommentStatVO> getCommentStats() {
|
||||
CommentStatVO statVO = new CommentStatVO();
|
||||
|
||||
// 获取各种统计数据
|
||||
// 1. 总评论数
|
||||
long totalComments = count();
|
||||
statVO.setCommentCount((int)totalComments);
|
||||
|
||||
// 2. 今日新增评论数
|
||||
LocalDate today = LocalDate.now();
|
||||
long todayComments = count(new LambdaQueryWrapper<Comment>()
|
||||
.ge(Comment::getCreateTime, today.atStartOfDay()));
|
||||
|
||||
// 3. 回复总数
|
||||
long totalReplies = commentReplyMapper.selectCount(null);
|
||||
statVO.setReplyCount((int)totalReplies);
|
||||
|
||||
// 4. 总点赞数
|
||||
long totalLikes = commentLikeMapper.selectCount(null);
|
||||
statVO.setLikeCount((int)totalLikes);
|
||||
|
||||
return Result.success(statVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将评论实体转换为VO
|
||||
*/
|
||||
private CommentVO convertToCommentVO(Comment comment, Long userId) {
|
||||
CommentVO vo = new CommentVO();
|
||||
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);
|
||||
}
|
||||
|
||||
// 获取点赞状态
|
||||
if (userId != null) {
|
||||
long likeCount = commentLikeMapper.checkUserLiked(comment.getId(), userId, 1);
|
||||
vo.setIsLiked(likeCount > 0);
|
||||
} else {
|
||||
vo.setIsLiked(false);
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package com.nuaa.video.comment.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 敏感词过滤工具类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SensitiveWordFilter {
|
||||
|
||||
/**
|
||||
* 敏感词匹配规则
|
||||
*/
|
||||
private static final int MinMatchType = 1; // 最小匹配规则
|
||||
private static final int MaxMatchType = 2; // 最大匹配规则
|
||||
|
||||
/**
|
||||
* 敏感词集合
|
||||
*/
|
||||
private static HashMap<String, Object> sensitiveWordMap;
|
||||
|
||||
/**
|
||||
* 初始化敏感词库
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
// 从资源文件夹读取敏感词列表
|
||||
Set<String> sensitiveWordSet = new HashSet<>();
|
||||
|
||||
// 这里简化处理,实际项目中可以从数据库或配置文件读取
|
||||
InputStream is = this.getClass().getResourceAsStream("/sensitive-words.txt");
|
||||
if (is != null) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.trim().length() > 0) {
|
||||
sensitiveWordSet.add(line.trim());
|
||||
}
|
||||
}
|
||||
reader.close();
|
||||
} else {
|
||||
// 添加一些默认敏感词
|
||||
sensitiveWordSet.add("敏感词1");
|
||||
sensitiveWordSet.add("敏感词2");
|
||||
sensitiveWordSet.add("敏感词3");
|
||||
}
|
||||
|
||||
// 初始化敏感词库
|
||||
sensitiveWordMap = new HashMap<>(sensitiveWordSet.size());
|
||||
for (String word : sensitiveWordSet) {
|
||||
if (word.trim().length() > 0) {
|
||||
addSensitiveWord(word);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("敏感词库初始化完成,共有敏感词:{} 个", sensitiveWordSet.size());
|
||||
} catch (IOException e) {
|
||||
log.error("初始化敏感词库失败", e);
|
||||
sensitiveWordMap = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加敏感词到HashMap中
|
||||
*/
|
||||
private void addSensitiveWord(String word) {
|
||||
// 获取敏感词
|
||||
char[] chars = word.toCharArray();
|
||||
|
||||
// 初始化敏感词库
|
||||
HashMap<String, Object> currentMap = sensitiveWordMap;
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
// 转换成char型
|
||||
char charKey = chars[i];
|
||||
// 获取
|
||||
HashMap<String, Object> wordMap = (HashMap<String, Object>) currentMap.get(String.valueOf(charKey));
|
||||
// 如果存在该key,直接赋值,用于下一个循环获取
|
||||
if (wordMap != null) {
|
||||
currentMap = wordMap;
|
||||
} else {
|
||||
// 不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
|
||||
HashMap<String, Object> newWordMap = new HashMap<>();
|
||||
// 不是最后一个
|
||||
newWordMap.put("isEnd", "0");
|
||||
// 添加到集合
|
||||
currentMap.put(String.valueOf(charKey), newWordMap);
|
||||
// 重新获取
|
||||
currentMap = newWordMap;
|
||||
}
|
||||
|
||||
// 如果是最后一个,则设置为结尾字
|
||||
if (i == chars.length - 1) {
|
||||
currentMap.put("isEnd", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文本中是否包含敏感词
|
||||
*/
|
||||
public boolean containsSensitiveWord(String text) {
|
||||
if (text == null || text.trim().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !getSensitiveWords(text, MinMatchType).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本中的敏感词
|
||||
*/
|
||||
public Set<String> getSensitiveWords(String text, int matchType) {
|
||||
Set<String> sensitiveWords = new HashSet<>();
|
||||
|
||||
if (sensitiveWordMap == null || sensitiveWordMap.isEmpty() || text == null) {
|
||||
return sensitiveWords;
|
||||
}
|
||||
|
||||
char[] chars = text.toCharArray();
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
int length = checkSensitiveWord(chars, i, matchType);
|
||||
if (length > 0) {
|
||||
sensitiveWords.add(text.substring(i, i + length));
|
||||
// 如果是最小匹配,直接跳过匹配的字符
|
||||
if (matchType == MinMatchType) {
|
||||
i += length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sensitiveWords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换敏感词
|
||||
*/
|
||||
public String replaceSensitiveWords(String text, String replacement) {
|
||||
if (text == null || text.trim().length() == 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
Set<String> sensitiveWords = getSensitiveWords(text, MaxMatchType);
|
||||
for (String word : sensitiveWords) {
|
||||
String replaceString = getReplacement(replacement, word.length());
|
||||
text = text.replace(word, replaceString);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成替换字符串
|
||||
*/
|
||||
private String getReplacement(String replacement, int length) {
|
||||
if (replacement == null || replacement.isEmpty()) {
|
||||
replacement = "*";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append(replacement);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查敏感词
|
||||
*/
|
||||
private int checkSensitiveWord(char[] chars, int beginIndex, int matchType) {
|
||||
boolean flag = false; // 敏感词结束标识位
|
||||
int matchFlag = 0; // 匹配标识数默认为0
|
||||
HashMap<String, Object> nowMap = sensitiveWordMap;
|
||||
|
||||
for (int i = beginIndex; i < chars.length; i++) {
|
||||
String key = String.valueOf(chars[i]);
|
||||
// 获取指定key
|
||||
nowMap = (HashMap<String, Object>) nowMap.get(key);
|
||||
// 如果存在,则判断是否为最后一个
|
||||
if (nowMap != null) {
|
||||
// 找到相应key,匹配标识+1
|
||||
matchFlag++;
|
||||
// 如果为最后一个匹配规则,结束循环,返回匹配标识数
|
||||
if ("1".equals(nowMap.get("isEnd"))) {
|
||||
// 结束标志位为true
|
||||
flag = true;
|
||||
// 最小规则,直接返回,最大规则还需继续查找
|
||||
if (MinMatchType == matchType) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不存在,直接返回
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 长度必须大于0,flag必须为true
|
||||
if (matchFlag < 1 || !flag) {
|
||||
matchFlag = 0;
|
||||
}
|
||||
|
||||
return matchFlag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.nuaa.video.comment.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 评论统计VO
|
||||
*/
|
||||
@Data
|
||||
public class CommentStatVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 评论总数
|
||||
*/
|
||||
private Integer commentCount;
|
||||
|
||||
/**
|
||||
* 回复总数
|
||||
*/
|
||||
private Integer replyCount;
|
||||
|
||||
/**
|
||||
* 点赞总数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.nuaa.video.comment.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 评论VO
|
||||
*/
|
||||
@Data
|
||||
public class CommentVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String userNickname;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 回复数
|
||||
*/
|
||||
private Integer replyCount;
|
||||
|
||||
/**
|
||||
* 置顶状态(0:普通,1:置顶)
|
||||
*/
|
||||
private Integer topStatus;
|
||||
|
||||
/**
|
||||
* 是否已点赞
|
||||
*/
|
||||
private Boolean isLiked;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 热门回复列表(前3条)
|
||||
*/
|
||||
private List<ReplyVO> hotReplies;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.nuaa.video.comment.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 回复VO
|
||||
*/
|
||||
@Data
|
||||
public class ReplyVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 回复ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
private Long commentId;
|
||||
|
||||
/**
|
||||
* 视频ID
|
||||
*/
|
||||
private Long videoId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String userNickname;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 回复目标用户ID
|
||||
*/
|
||||
private Long replyToId;
|
||||
|
||||
/**
|
||||
* 回复目标用户昵称
|
||||
*/
|
||||
private String replyToNickname;
|
||||
|
||||
/**
|
||||
* 回复目标用户头像
|
||||
*/
|
||||
private String replyToAvatar;
|
||||
|
||||
/**
|
||||
* 回复内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 是否已点赞
|
||||
*/
|
||||
private Boolean isLiked;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
server:
|
||||
port: 9005
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: video-comment
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: ${SENTINEL_HOST:sentinel}:${SENTINEL_PORT:8080}
|
||||
port: 8725
|
||||
eager: true
|
||||
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
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: redis_EsJ8FX
|
||||
database: 4
|
||||
timeout: 10000
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 200
|
||||
max-wait: -1
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: com.nuaa.video.comment.entity
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
server:
|
||||
port: 9004
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: video-comment
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:common.yaml?refreshEnabled=true
|
||||
- optional:nacos:common-redis-${spring.profiles.active}.yaml?refreshEnabled=true
|
||||
- optional:nacos:${spring.application.name}.yaml?refreshEnabled=true
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
ip: ${SPRING_CLOUD_NACOS_DISCOVERY_IP:}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
namespace: ${spring.cloud.nacos.discovery.namespace}
|
||||
group: ${spring.cloud.nacos.discovery.group}
|
||||
file-extension: yaml
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
@@ -0,0 +1,23 @@
|
||||
spring:
|
||||
application:
|
||||
name: video-comment
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
ephemeral: true
|
||||
ip: ${COMMENT_IP:}
|
||||
port: ${server.port:9005}
|
||||
fail-fast: true
|
||||
register-enabled: true
|
||||
config:
|
||||
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
file-extension: yaml
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.comment.mapper.CommentLikeMapper">
|
||||
|
||||
<!-- 检查用户是否已点赞 -->
|
||||
<select id="checkUserLiked" resultType="long">
|
||||
SELECT COUNT(1)
|
||||
FROM comment_like
|
||||
WHERE comment_id = #{commentId}
|
||||
AND user_id = #{userId}
|
||||
AND type = #{type}
|
||||
</select>
|
||||
|
||||
<!-- 统计视频评论点赞数 -->
|
||||
<select id="countVideoCommentLikes" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM comment_like cl
|
||||
LEFT JOIN comment c ON cl.comment_id = c.id AND cl.type = 1
|
||||
LEFT JOIN comment_reply cr ON cl.comment_id = cr.id AND cl.type = 2
|
||||
WHERE (c.video_id = #{videoId} OR cr.video_id = #{videoId})
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.comment.mapper.CommentMapper">
|
||||
|
||||
<!-- 分页查询评论列表 -->
|
||||
<select id="selectCommentList" resultType="com.nuaa.video.comment.entity.Comment">
|
||||
SELECT *
|
||||
FROM comment
|
||||
<where>
|
||||
status = 1
|
||||
<if test="query.videoId != null">
|
||||
AND video_id = #{query.videoId}
|
||||
</if>
|
||||
<if test="query.userId != null">
|
||||
AND user_id = #{query.userId}
|
||||
</if>
|
||||
</where>
|
||||
<choose>
|
||||
<when test="query.orderBy != null and query.orderBy != ''">
|
||||
ORDER BY top_status DESC, ${query.orderBy}
|
||||
<if test="query.orderDirection != null and query.orderDirection != ''">
|
||||
${query.orderDirection}
|
||||
</if>
|
||||
</when>
|
||||
<otherwise>
|
||||
ORDER BY top_status DESC, create_time DESC
|
||||
</otherwise>
|
||||
</choose>
|
||||
</select>
|
||||
|
||||
<!-- 增加点赞数 -->
|
||||
<update id="increaseLikeCount">
|
||||
UPDATE comment
|
||||
SET like_count = like_count + #{count}
|
||||
WHERE id = #{commentId}
|
||||
</update>
|
||||
|
||||
<!-- 增加回复数 -->
|
||||
<update id="increaseReplyCount">
|
||||
UPDATE comment
|
||||
SET reply_count = reply_count + #{count}
|
||||
WHERE id = #{commentId}
|
||||
</update>
|
||||
|
||||
<!-- 统计视频评论数 -->
|
||||
<select id="countVideoComments" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM comment
|
||||
WHERE video_id = #{videoId}
|
||||
AND status = 1
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.comment.mapper.CommentReplyMapper">
|
||||
|
||||
<!-- 查询评论的回复列表 -->
|
||||
<select id="selectCommentReplyList" resultType="com.nuaa.video.comment.entity.CommentReply">
|
||||
SELECT *
|
||||
FROM comment_reply
|
||||
WHERE comment_id = #{commentId}
|
||||
AND status = 1
|
||||
ORDER BY create_time ASC
|
||||
</select>
|
||||
|
||||
<!-- 获取评论的热门回复 -->
|
||||
<select id="selectHotReplies" resultType="com.nuaa.video.comment.entity.CommentReply">
|
||||
SELECT *
|
||||
FROM comment_reply
|
||||
WHERE comment_id = #{commentId}
|
||||
AND status = 1
|
||||
ORDER BY like_count DESC, create_time ASC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 增加点赞数 -->
|
||||
<update id="increaseLikeCount">
|
||||
UPDATE comment_reply
|
||||
SET like_count = like_count + #{count}
|
||||
WHERE id = #{replyId}
|
||||
</update>
|
||||
|
||||
<!-- 统计视频回复数 -->
|
||||
<select id="countVideoReplies" resultType="int">
|
||||
SELECT COUNT(1)
|
||||
FROM comment_reply
|
||||
WHERE video_id = #{videoId}
|
||||
AND status = 1
|
||||
</select>
|
||||
</mapper>
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package com.nuaa.video.comment.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.nuaa.video.comment.dto.CommentAddDTO;
|
||||
import com.nuaa.video.comment.dto.CommentQueryDTO;
|
||||
import com.nuaa.video.comment.entity.Comment;
|
||||
import com.nuaa.video.comment.entity.CommentLike;
|
||||
import com.nuaa.video.comment.mapper.CommentLikeMapper;
|
||||
import com.nuaa.video.comment.mapper.CommentMapper;
|
||||
import com.nuaa.video.comment.service.impl.CommentServiceImpl;
|
||||
import com.nuaa.video.comment.vo.CommentVO;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* CommentService 单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("评论服务测试")
|
||||
class CommentServiceTest {
|
||||
|
||||
@Mock
|
||||
private CommentMapper commentMapper;
|
||||
|
||||
@Mock
|
||||
private CommentLikeMapper commentLikeMapper;
|
||||
|
||||
@InjectMocks
|
||||
private CommentServiceImpl commentService;
|
||||
|
||||
private Comment testComment;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testComment = new Comment();
|
||||
testComment.setId(1L);
|
||||
testComment.setVideoId(1L);
|
||||
testComment.setUserId(1L);
|
||||
testComment.setContent("这是一条测试评论");
|
||||
testComment.setLikeCount(10);
|
||||
testComment.setReplyCount(5);
|
||||
testComment.setTopStatus(0);
|
||||
testComment.setStatus(1);
|
||||
testComment.setCreateTime(LocalDateTime.now());
|
||||
testComment.setUpdateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("添加评论 - 成功")
|
||||
void testAddCommentSuccess() {
|
||||
// Given
|
||||
CommentAddDTO dto = new CommentAddDTO();
|
||||
dto.setVideoId(1L);
|
||||
dto.setContent("这是一条新评论");
|
||||
|
||||
Long userId = 1L;
|
||||
|
||||
when(commentMapper.insert(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// Then
|
||||
assertNotNull(dto);
|
||||
assertEquals(1L, dto.getVideoId());
|
||||
assertEquals("这是一条新评论", dto.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("添加评论 - 内容为空")
|
||||
void testAddCommentEmptyContent() {
|
||||
// Given
|
||||
CommentAddDTO dto = new CommentAddDTO();
|
||||
dto.setVideoId(1L);
|
||||
dto.setContent("");
|
||||
|
||||
// Then
|
||||
assertTrue(dto.getContent().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取评论列表 - 成功")
|
||||
void testGetCommentListSuccess() {
|
||||
// Given
|
||||
CommentQueryDTO queryDTO = new CommentQueryDTO();
|
||||
queryDTO.setVideoId(1L);
|
||||
queryDTO.setPageNum(1);
|
||||
queryDTO.setPageSize(10);
|
||||
|
||||
List<Comment> commentList = new ArrayList<>();
|
||||
commentList.add(testComment);
|
||||
|
||||
// Then
|
||||
assertNotNull(queryDTO);
|
||||
assertEquals(1L, queryDTO.getVideoId());
|
||||
assertEquals(1, queryDTO.getPageNum());
|
||||
assertEquals(10, queryDTO.getPageSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取评论详情 - 成功")
|
||||
void testGetCommentDetailSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 1L;
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNotNull(comment);
|
||||
assertEquals("这是一条测试评论", comment.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取评论详情 - 评论不存在")
|
||||
void testGetCommentDetailNotFound() {
|
||||
// Given
|
||||
Long commentId = 999L;
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(null);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNull(comment);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除评论 - 成功")
|
||||
void testDeleteCommentSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 1L;
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentMapper.updateById(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNotNull(comment);
|
||||
assertEquals(userId, comment.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除评论 - 非作者无法删除")
|
||||
void testDeleteCommentNotOwner() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 2L; // 非作者
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNotNull(comment);
|
||||
assertNotEquals(userId, comment.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("点赞评论 - 成功")
|
||||
void testLikeCommentSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 2L;
|
||||
int oldLikeCount = testComment.getLikeCount();
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentLikeMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null); // 未点赞过
|
||||
when(commentLikeMapper.insert(any(CommentLike.class))).thenReturn(1);
|
||||
when(commentMapper.updateById(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// Then
|
||||
assertNotNull(testComment);
|
||||
assertEquals(10, oldLikeCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("点赞评论 - 已点赞过")
|
||||
void testLikeCommentAlreadyLiked() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 2L;
|
||||
|
||||
CommentLike existingLike = new CommentLike();
|
||||
existingLike.setCommentId(commentId);
|
||||
existingLike.setUserId(userId);
|
||||
existingLike.setType(1); // 评论类型
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentLikeMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existingLike);
|
||||
|
||||
// Then
|
||||
assertNotNull(existingLike);
|
||||
assertEquals(commentId, existingLike.getCommentId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("取消点赞评论 - 成功")
|
||||
void testUnlikeCommentSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 2L;
|
||||
|
||||
CommentLike existingLike = new CommentLike();
|
||||
existingLike.setId(1L);
|
||||
existingLike.setCommentId(commentId);
|
||||
existingLike.setUserId(userId);
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentLikeMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existingLike);
|
||||
when(commentLikeMapper.deleteById(any(Long.class))).thenReturn(1);
|
||||
when(commentMapper.updateById(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// Then
|
||||
assertNotNull(existingLike);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("置顶评论 - 成功")
|
||||
void testTopCommentSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 1L;
|
||||
Long videoId = 1L;
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentMapper.updateById(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNotNull(comment);
|
||||
assertEquals(0, comment.getTopStatus()); // 置顶前
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("取消置顶评论 - 成功")
|
||||
void testUntopCommentSuccess() {
|
||||
// Given
|
||||
Long commentId = 1L;
|
||||
Long userId = 1L;
|
||||
Long videoId = 1L;
|
||||
|
||||
testComment.setTopStatus(1); // 已置顶
|
||||
|
||||
when(commentMapper.selectById(commentId)).thenReturn(testComment);
|
||||
when(commentMapper.updateById(any(Comment.class))).thenReturn(1);
|
||||
|
||||
// When
|
||||
Comment comment = commentMapper.selectById(commentId);
|
||||
|
||||
// Then
|
||||
assertNotNull(comment);
|
||||
assertEquals(1, comment.getTopStatus()); // 已置顶状态
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取评论统计 - 成功")
|
||||
void testGetCommentStatSuccess() {
|
||||
// Given
|
||||
Long videoId = 1L;
|
||||
|
||||
// Then
|
||||
assertNotNull(videoId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
spring:
|
||||
application:
|
||||
name: video-comment-test
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.nuaa.video: DEBUG
|
||||
org.springframework: INFO
|
||||
|
||||
# Nacos配置(测试环境禁用)
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
# Sentinel配置(测试环境禁用)
|
||||
spring:
|
||||
cloud:
|
||||
sentinel:
|
||||
enabled: false
|
||||
Reference in New Issue
Block a user