评论回复 评论树形列表 评论点赞 回复点赞 具体实现思路。
最近做的社交ai项目 涉及到评论回复这一功能模块的实现。
数据层用的jpa
-- mixmix.t_agents_comment definition CREATE TABLE `t_agents_comment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '评论ID', `content` text NOT NULL COMMENT '评论内容', `user_id` bigint(20) NOT NULL COMMENT '评论用户ID', `target_id` bigint(20) NOT NULL COMMENT '评论目标ID(如文章ID、视频ID等)', `target_type` tinyint(4) NOT NULL COMMENT '评论目标类型(1.智能体评论 2.文章等等)', `like_count` int(11) DEFAULT '0' COMMENT '点赞数', `reply_count` int(11) DEFAULT '0' COMMENT '回复数', `status` tinyint(4) DEFAULT '1' COMMENT '状态(0:删除 1:正常)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_target` (`target_id`,`target_type`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
-- mixmix.t_agents_comment_like definition CREATE TABLE `t_agents_comment_like` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `comment_id` bigint(20) NOT NULL COMMENT '评论ID', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_comment_user` (`comment_id`,`user_id`) COMMENT '评论用户联合唯一索引' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='评论点赞表';
-- mixmix.t_agents_reply definition CREATE TABLE `t_agents_reply` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '回复ID', `content` text NOT NULL COMMENT '回复内容', `comment_id` bigint(20) NOT NULL COMMENT '原评论ID', `reply_id` bigint(20) DEFAULT NULL COMMENT '回复ID', `user_id` bigint(20) NOT NULL COMMENT '回复用户ID', `reply_user_id` bigint(20) DEFAULT NULL COMMENT '被回复用户ID', `like_count` int(11) DEFAULT '0' COMMENT '点赞数', `reply_level` int(11) DEFAULT '1' COMMENT '回复层级(1级为直接回复评论,2级及以上为回复其他回复)', `status` tinyint(4) DEFAULT '1' COMMENT '状态(0:删除 1:正常)', `parent_id` bigint(20) DEFAULT NULL COMMENT '父回复ID(回复其他回复时使用)', `root_id` bigint(20) DEFAULT NULL COMMENT '回复树根ID(用于优化查询)', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_comment` (`comment_id`), KEY `idx_parent` (`parent_id`), KEY `idx_user` (`user_id`), KEY `idx_root` (`root_id`) ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='评论回复表';
-- mixmix.t_agents_reply_like definition CREATE TABLE `t_agents_reply_like` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `reply_id` bigint(20) NOT NULL COMMENT '回复ID', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_reply_user` (`reply_id`,`user_id`) COMMENT '回复用户联合唯一索引' ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='回复点赞记录表';
主要是评论回复表,评论表中的target_id 就是要评论的实体(如评论智能体,评论文章等等),每条评论也会有自己的回复总数和点赞总数,此处需要考虑线程安全问题。回复表中有评论id号作为关联,reply_id相当于paraentid,这张表parentid rootid暂时没用到,当评论时 如果是第一条评论 ,不需要传reply_id,这里也是有点赞数的统计。
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentDTO { private Long id; private Long userId; private Long targetId; private Integer targetType; private String content; private Integer likeCount; private Integer replyCount; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; // 用户信息,需要根据实际情况补充 private String user; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentReplyDTO { private Long id; private Long commentId; private Long userId; private Long replyId; private Long replyUserId; private String content; private Integer likeCount; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; // 用户信息,需要根据实际情况补充 private String user; private String replyUser; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentReplyTreeDTO { private Long id; private Long commentId; private Long userId; private Long replyId; private Long replyUserId; private String content; private Integer likeCount; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; private List<CommentReplyTreeDTO> children; private String avatarUrl; private String nickname; private String userName; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentTreeDTO { private Long id; private Long userId; private String avatarUrl; private String nickname; private String userName; private Long targetId; private Integer targetType; private String content; private Integer likeCount; private Integer replyCount; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; private List<CommentReplyTreeDTO> replies; } @Entity @Table(name = "t_agents_comment") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "target_id", nullable = false) private Long targetId; @Column(name = "target_type", nullable = false) private Integer targetType; @Column(nullable = false) private String content; @Column(name = "like_count") private Integer likeCount; @Column(name = "reply_count") private Integer replyCount; @Column(nullable = false) private Integer status; @Column(name = "create_time", nullable = false) private LocalDateTime createTime; @Column(name = "update_time", nullable = false) private LocalDateTime updateTime; } @Entity @Table(name = "t_agents_comment_like", uniqueConstraints = { @UniqueConstraint(columnNames = {"comment_id", "user_id"}) }) @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "comment_id", nullable = false) private Long commentId; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "create_time", nullable = false) private LocalDateTime createTime; } @Entity @Table(name = "t_agents_reply") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentReply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "comment_id", nullable = false) private Long commentId; @Column(name = "parent_id") private Long parentId; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "reply_id") private Long replyId; @Column(name = "reply_user_id") private Long replyUserId; @Column(nullable = false) private String content; @Column(name = "like_count") private Integer likeCount; @Column(nullable = false) private Integer status; @Column(name = "reply_level", nullable = false) private Integer replyLevel; @Column(name = "create_time", nullable = false) private LocalDateTime createTime; @Column(name = "update_time", nullable = false) private LocalDateTime updateTime; }@Entity @Table(name = "t_agents_reply_like", uniqueConstraints = { @UniqueConstraint(columnNames = {"reply_id", "user_id"}) }) @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ReplyLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "reply_id", nullable = false) private Long replyId; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "create_time", nullable = false) private LocalDateTime createTime; } public interface TAgentsCommentLikeRepo extends EnhancedRepo<CommentLike, Long> { boolean existsByCommentIdAndUserId(Long commentId, Long userId); } public interface TAgentsCommentRepo extends EnhancedRepo<Comment, Long> { @Query("SELECT c FROM Comment c WHERE c.targetId = :targetId AND c.targetType = :targetType AND c.status = 1 ORDER BY c.createTime asc") Page<Comment> findByTargetIdAndType( @Param("targetId") Long targetId, @Param("targetType") Integer targetType, Pageable pageable ); @Query("SELECT COUNT(c) > 0 FROM CommentLike c WHERE c.commentId = :commentId AND c.userId = :userId") boolean existsByCommentIdAndUserId(@Param("commentId") Long commentId, @Param("userId") Long userId); @Modifying @Query("UPDATE Comment c SET c.likeCount = c.likeCount + 1 WHERE c.id = :commentId") void incrementLikeCount(@Param("commentId") Long commentId); @Modifying @Query("UPDATE Comment c SET c.replyCount = c.replyCount + 1 WHERE c.id = :commentId") void incrementReplyCount(@Param("commentId") Long commentId); @Modifying @Query("UPDATE Comment c SET c.replyCount = c.replyCount - 1 WHERE c.id = :commentId AND c.replyCount > 0") void decrementReplyCount(@Param("commentId") Long commentId); } public interface TAgentsReplyLikeRepo extends EnhancedRepo<ReplyLike, Long> { boolean existsByReplyIdAndUserId(Long replyId, Long userId); } public interface TAgentsReplyRepo extends EnhancedRepo<CommentReply, Long> { @Query("SELECT r FROM CommentReply r WHERE r.commentId = :commentId AND r.status = 1 ORDER BY r.createTime asc") Page<CommentReply> findByCommentId(@Param("commentId") Long commentId, Pageable pageable); @Query("SELECT COUNT(c) > 0 FROM ReplyLike c WHERE c.replyId = :replyId AND c.userId = :userId") boolean existsByReplyIdAndUserId(@Param("replyId") Long replyId, @Param("userId") Long userId); @Modifying @Query("UPDATE CommentReply r SET r.likeCount = r.likeCount + 1 WHERE r.id = :replyId") void incrementLikeCount(@Param("replyId") Long replyId); @Modifying @Query("UPDATE CommentReply r SET r.status = :status WHERE r.commentId = :commentId") void updateStatusByCommentId(@Param("commentId") Long commentId, @Param("status") Integer status); /** * 获取评论下的所有回复 * * @param commentId 评论ID * @return 回复列表 */ @Query("SELECT r FROM CommentReply r WHERE r.commentId = :commentId AND r.status = 1 ORDER BY r.createTime asc") List<CommentReply> findAllByCommentId(@Param("commentId") Long commentId); } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentReplyRequest { @NotNull(message = "评论ID不能为空") private Long commentId; @NotNull(message = "用户ID不能为空") private Long userId; private Long replyId; private Long parentId; @NotBlank(message = "回复内容不能为空") @Length(max = 500, message = "回复内容不能超过500字") private String content; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CommentRequest { @NotNull(message = "用户ID不能为空") private Long userId; @NotNull(message = "目标ID不能为空") private Long targetId; private Integer targetType = 1; @NotBlank(message = "评论内容不能为空") @Length(max = 500, message = "评论内容不能超过500字") private String content; }
上边是vo和dto request repo数据层
@ModelService(ModelEnum.UNDEFINED) @Slf4j public class CommentServiceImpl { @Autowired private TAgentsCommentRepo commentRepository; @Autowired private TAgentsReplyRepo replyRepository; @Autowired private TAgentsCommentLikeRepo commentLikeRepository; @Autowired private TAgentsReplyLikeRepo replyLikeRepository; @Autowired private PcMemberRepo pcMemberRepo; @Transactional public CommentDTO createComment(CommentRequest request) { // 参数校验 if (StringUtils.isBlank(request.getContent())) { throw new IllegalArgumentException("评论内容不能为空"); } // 构建评论实体 Comment comment = Comment.builder() .userId(request.getUserId()) .targetId(request.getTargetId()) .targetType(request.getTargetType()) .content(request.getContent()) .createTime(LocalDateTime.now()) .updateTime(LocalDateTime.now()) .likeCount(0) .replyCount(0) .status(1) .build(); // 保存评论 comment = commentRepository.save(comment); // 转换为DTO返回 return convertToDTO(comment); } @Transactional public CommentReplyDTO createReply(CommentReplyRequest request) { // 参数校验 if (StringUtils.isBlank(request.getContent())) { throw new IllegalArgumentException("回复内容不能为空"); } // 检查评论是否存在 Comment comment = commentRepository.findById(request.getCommentId()) .orElseThrow(() -> new IllegalArgumentException("评论不存在")); Long replyId = request.getReplyId(); int replyLevel = 1; if (replyId != null && replyId >= 0) { CommentReply commentReply = replyRepository.findById(replyId) .orElseThrow(() -> new IllegalArgumentException("回复目标不存在")); replyLevel = 2; } // 构建回复实体 CommentReply.CommentReplyBuilder builder = CommentReply.builder() .commentId(request.getCommentId()) .userId(request.getUserId()) .content(request.getContent()) .createTime(LocalDateTime.now()) .updateTime(LocalDateTime.now()) .likeCount(0) .replyLevel(replyLevel) .status(1); // 设置回复目标 if (request.getReplyId() != null) { CommentReply targetReply = replyRepository.findById(request.getReplyId()) .orElseThrow(() -> new IllegalArgumentException("回复的目标不存在")); builder.replyId(request.getReplyId()) .replyUserId(targetReply.getUserId()); } // 保存回复 CommentReply reply = replyRepository.save(builder.build()); // 更新评论的回复数 commentRepository.incrementReplyCount(comment.getId()); // 转换为DTO返回 return convertToReplyDTO(reply); } public List<CommentDTO> getCommentTree(Long targetId, Integer targetType, Integer page, Integer size) { // 分页查询评论列表 PageRequest pageRequest = PageRequest.of(page - 1, size); Page<Comment> commentPage = commentRepository.findByTargetIdAndType(targetId, targetType, pageRequest); // 转换为DTO return commentPage.getContent().stream() .map(this::convertToDTO) .collect(Collectors.toList()); } public List<CommentReplyDTO> getReplyList(Long commentId, Integer page, Integer size) { // 分页查询回复列表 PageRequest pageRequest = PageRequest.of(page - 1, size); Page<CommentReply> replyPage = replyRepository.findByCommentId(commentId, pageRequest); // 转换为DTO return replyPage.getContent().stream() .map(this::convertToReplyDTO) .collect(Collectors.toList()); } @Transactional public void deleteComment(Long commentId, Long userId) { Comment comment = commentRepository.findById(commentId) .orElseThrow(() -> new IllegalArgumentException("评论不存在")); // 检查权限 if (!comment.getUserId().equals(userId)) { throw new IllegalArgumentException("无权删除该评论"); } // 删除评论(软删除) comment.setStatus(0); comment.setUpdateTime(LocalDateTime.now()); commentRepository.save(comment); // 删除关联的回复 replyRepository.updateStatusByCommentId(commentId, 0); } @Transactional public void deleteReply(Long replyId, Long userId) { CommentReply reply = replyRepository.findById(replyId) .orElseThrow(() -> new IllegalArgumentException("回复不存在")); // 检查权限 if (!reply.getUserId().equals(userId)) { throw new IllegalArgumentException("无权删除该回复"); } // 删除回复(软删除) reply.setStatus(0); reply.setUpdateTime(LocalDateTime.now()); replyRepository.save(reply); // 更新评论的回复数 commentRepository.decrementReplyCount(reply.getCommentId()); } private CommentDTO convertToDTO(Comment comment) { if (comment == null) { return null; } return CommentDTO.builder() .id(comment.getId()) .userId(comment.getUserId()) .targetId(comment.getTargetId()) .targetType(comment.getTargetType()) .content(comment.getContent()) .likeCount(comment.getLikeCount()) .replyCount(comment.getReplyCount()) .createTime(comment.getCreateTime()) .build(); } private CommentReplyDTO convertToReplyDTO(CommentReply reply) { if (reply == null) { return null; } return CommentReplyDTO.builder() .id(reply.getId()) .commentId(reply.getCommentId()) .userId(reply.getUserId()) .replyId(reply.getReplyId()) .replyUserId(reply.getReplyUserId()) .content(reply.getContent()) .likeCount(reply.getLikeCount()) .createTime(reply.getCreateTime()) .build(); } @Transactional public void likeComment(Long commentId, Long userId) { // 检查评论是否存在 Comment comment = commentRepository.findById(commentId) .orElseThrow(() -> new IllegalArgumentException("评论不存在")); // 检查是否已点赞 if (commentLikeRepository.existsByCommentIdAndUserId(commentId, userId)) { throw new IllegalArgumentException("已经点赞过该评论"); } // 添加点赞记录 CommentLike like = CommentLike.builder() .commentId(commentId) .userId(userId) .createTime(LocalDateTime.now()) .build(); commentLikeRepository.save(like); // 更新点赞数 commentRepository.incrementLikeCount(commentId); } @Transactional public void likeReply(Long replyId, Long userId) { // 检查回复是否存在 CommentReply reply = replyRepository.findById(replyId) .orElseThrow(() -> new IllegalArgumentException("回复不存在")); // 检查是否已点赞 if (replyLikeRepository.existsByReplyIdAndUserId(replyId, userId)) { throw new IllegalArgumentException("已经点赞过该回复"); } // 添加点赞记录 ReplyLike like = ReplyLike.builder() .replyId(replyId) .userId(userId) .createTime(LocalDateTime.now()) .build(); replyLikeRepository.save(like); // 更新点赞数 replyRepository.incrementLikeCount(replyId); } public List<CommentTreeDTO> getCommentTreeList(Long targetId, Integer targetType, Integer page, Integer size) { // 获取评论列表 PageRequest pageRequest = PageRequest.of(page - 1, size); Page<Comment> commentPage = commentRepository.findByTargetIdAndType(targetId, targetType, pageRequest); List<Comment> content = commentPage.getContent(); if (CollectionUtils.isEmpty(content)) { return Collections.emptyList(); } // 转换为树形结构 return content.stream() .map(comment -> { Long userId = comment.getUserId(); List<PcMember> member = pcMemberRepo.findAll(SpecTool.and(SpecTool.eq(PcMember_.id, userId.intValue()))); PcMember pcMember = member.get(0); CommentTreeDTO dto = CommentTreeDTO.builder() .id(comment.getId()) .userId(comment.getUserId()) .targetId(comment.getTargetId()) .targetType(comment.getTargetType()) .content(comment.getContent()) .likeCount(comment.getLikeCount()) .replyCount(comment.getReplyCount()) .createTime(comment.getCreateTime()) .avatarUrl(pcMember.getNewImg()) .nickname(pcMember.getNickname()) .userName(pcMember.getGagaNo()) .build(); // 获取该评论的所有回复 List<CommentReply> replies = replyRepository.findAllByCommentId(comment.getId()); dto.setReplies(buildReplyTree(replies)); return dto; }) .collect(Collectors.toList()); } private List<CommentReplyTreeDTO> buildReplyTree(List<CommentReply> replies) { // 收集所有涉及的用户ID Set<Long> userIds = new HashSet<>(); replies.forEach(reply -> { userIds.add(reply.getUserId()); if (reply.getReplyUserId() != null) { userIds.add(reply.getReplyUserId()); } }); // 批量查询所有用户信息 Map<Long, PcMember> userMap = new HashMap<>(); if (!userIds.isEmpty()) { List<PcMember> members = pcMemberRepo.findAll( SpecTool.and(SpecTool.in(PcMember_.id, userIds.stream().map(Long::intValue).collect(Collectors.toList()))) ); members.forEach(member -> userMap.put((long) member.getId(), member)); } // 构建回复ID到回复对象的映射,并关联用户信息 Map<Long, CommentReplyTreeDTO> replyMap = replies.stream() .map(reply -> { // 从缓存中获取用户信息 PcMember pcMember = userMap.get(reply.getUserId()); if (pcMember == null) { pcMember = new PcMember(); // 默认值或空对象 } // 构建 DTO return CommentReplyTreeDTO.builder() .id(reply.getId()) .commentId(reply.getCommentId()) .userId(reply.getUserId()) .replyId(reply.getReplyId()) .replyUserId(reply.getReplyUserId()) .content(reply.getContent()) .likeCount(reply.getLikeCount()) .createTime(reply.getCreateTime()) .avatarUrl(pcMember.getNewImg()) .nickname(pcMember.getNickname()) .userName(pcMember.getGagaNo()) .children(new ArrayList<>()) .build(); }) .collect(Collectors.toMap(CommentReplyTreeDTO::getId, dto -> dto)); // 构建树形结构 List<CommentReplyTreeDTO> rootReplies = new ArrayList<>(); replyMap.values().forEach(reply -> { if (reply.getReplyId() == null) { rootReplies.add(reply); } else { CommentReplyTreeDTO parent = replyMap.get(reply.getReplyId()); if (parent != null) { parent.getChildren().add(reply); } } }); return rootReplies; }}
@RestController @RequestMapping("/comments") @Slf4j @Tag(name = "评论管理", description = "评论、回复及点赞相关接口") @Validated public class CommentController { @Autowired private CommentServiceImpl commentService; @PostMapping @Operation(summary = "发表评论") public R<CommentDTO> createComment(@RequestBody @Valid CommentRequest request) { return R.success(commentService.createComment(request)); } @PostMapping("/replies") @Operation(summary = "发表回复") public R<CommentReplyDTO> createReply( @RequestBody @Valid CommentReplyRequest request) { return R.success(commentService.createReply(request)); } @GetMapping("/tree") @Operation(summary = "获取评论树形列表") public R<List<CommentTreeDTO>> getCommentTree( @RequestParam Long targetId, @RequestParam(defaultValue = "1") Integer targetType, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return R.success(commentService.getCommentTreeList(targetId, targetType, page, size)); } @GetMapping @Operation(summary = "获取评论列表") public R<List<CommentDTO>> getComments( @RequestParam Long targetId, @RequestParam(defaultValue = "1") Integer targetType, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return R.success(commentService.getCommentTree(targetId, targetType, page, size)); } @GetMapping("/{commentId}/replies") @Operation(summary = "获取回复列表") public R<List<CommentReplyDTO>> getReplies( @PathVariable Long commentId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return R.success(commentService.getReplyList(commentId, page, size)); } @DeleteMapping("/{commentId}") @Operation(summary = "删除评论") public R<Void> deleteComment( @PathVariable Long commentId, @RequestParam Long userId) { commentService.deleteComment(commentId, userId); return R.success(); } @DeleteMapping("/replies/{replyId}") @Operation(summary = "删除回复") public R<Void> deleteReply( @PathVariable Long replyId, @RequestParam Long userId) { commentService.deleteReply(replyId, userId); return R.success(); } @PostMapping("/{commentId}/like") @Operation(summary = "点赞评论") public R<Void> likeComment( @PathVariable Long commentId, @RequestParam Long userId) { commentService.likeComment(commentId, userId); return R.success(); } @PostMapping("/replies/{replyId}/like") @Operation(summary = "点赞回复") public R<Void> likeReply( @PathVariable Long replyId, @RequestParam Long userId) { commentService.likeReply(replyId, userId); return R.success(); } }
getCommentTree这个接口就是包含评论和所有回复的树形列表。
浙公网安备 33010602011771号