详解Java社区项目实现发帖、评论的方法(仿百度贴吧)

173 阅读5分钟

场景描述:Java社区项目,用户可以发帖,评论,仿百度贴吧,评论共二级评论,有帖子表(需记录1楼的标题和内容)和评论表,评论表逻辑外键关联帖子表,评论表设置父id,一级评论是顶层没有父id。二级评论即评论的评论就是楼中楼,其父id是该评论回复的评论id,楼中楼默认最多显示2条,可以点展开阅读全部楼中楼,分页展示,楼中楼同样是按时间排序的,发的早的排在前面,虽然前端展示出来的样式是共二级评论,不过可以一直回复某评论,一直记录父id(这个评论回复的评论id)即可实现。前端大致展示样式如下(展开了2楼的楼中楼的情况):

(1楼)(标题)讨论Java的帖子

(内容)XXXXXJava的发展前景XXXXX

(2楼)小明:XXX

(2楼的楼中楼)小A 回复 @小明:XXX

(2楼的楼中楼)小A 回复 @小明:XXX

(2楼的楼中楼)小B 回复 @小A:XXX

(2楼的楼中楼)小A 回复 @小B:XXX

(3楼)小王:XXX

(3楼的楼中楼)小A 回复 @小王:XXX

(4楼)小C:XXX

以下是基于Java的社区功能实现方案,采用Spring Boot + MyBatis Plus + MySQL技术栈,完整实现帖子和二级评论功能:

数据库设计

-- 帖子表
CREATE TABLE `post` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `title` VARCHAR(100) NOT NULL COMMENT '标题',
  `content` TEXT NOT NULL COMMENT '内容',
  `user_id` BIGINT NOT NULL COMMENT '发帖人ID',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

-- 评论表
CREATE TABLE `comment` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `post_id` BIGINT NOT NULL COMMENT '所属帖子ID',
  `user_id` BIGINT NOT NULL COMMENT '评论人ID',
  `content` VARCHAR(500) NOT NULL COMMENT '评论内容',
  `parent_id` BIGINT DEFAULT NULL COMMENT '父评论ID(一级评论为null)',
  `root_id` BIGINT DEFAULT NULL COMMENT '根评论ID(一级评论为自身ID)',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_post_id` (`post_id`),
  KEY `idx_root_id` (`root_id`)
) COMMENT='root_id用于快速查询一级评论下的所有回复';

实体类设计

// 帖子实体
@Data
public class Post {
    private Long id;
    private String title;
    private String content;
    private Long userId;
    private LocalDateTime createTime;
}

// 评论实体
@Data
public class Comment {
    private Long id;
    private Long postId;
    private Long userId;
    private String content;
    private Long parentId;   // 直接父ID
    private Long rootId;     // 所属一级评论ID
    private LocalDateTime createTime;
    
    // 非数据库字段
    private List<Comment> replies = new ArrayList<>(); // 楼中楼列表
    private String username;       // 评论人姓名
    private String parentUsername; // 被回复人姓名
}

核心实现逻辑

1. 发表评论服务

@Service
public class CommentService {
    @Autowired private CommentMapper commentMapper;

    public void addComment(Comment comment) {
        // 一级评论处理
        if (comment.getParentId() == null) {
            comment.setRootId(null); // 先插入再更新rootId
            commentMapper.insert(comment);
            comment.setRootId(comment.getId());
            commentMapper.updateById(comment);
        } 
        // 二级评论处理
        else {
            Comment parent = commentMapper.selectById(comment.getParentId());
            comment.setRootId(parent.getRootId() != null ? parent.getRootId() : parent.getId());
            commentMapper.insert(comment);
        }
    }
}

2. 获取帖子详情+分层评论

public PostVO getPostDetail(Long postId) {
    // 1. 获取帖子主体
    Post post = postMapper.selectById(postId);
    
    // 2. 获取所有一级评论(parent_id为null)
    List<Comment> firstLevelComments = commentMapper.selectList(
        new QueryWrapper<Comment>()
            .eq("post_id", postId)
            .isNull("parent_id")
            .orderByAsc("create_time")
    );
    
    // 3. 批量获取一级评论下的楼中楼(按root_id分组查询)
    List<Long> rootIds = firstLevelComments.stream().map(Comment::getId).collect(Collectors.toList());
    if (!rootIds.isEmpty()) {
        // 查询所有关联的二级评论
        List<Comment> allReplies = commentMapper.selectList(
            new QueryWrapper<Comment>()
                .in("root_id", rootIds)
                .orderByAsc("create_time")
        );
        
        // 按root_id分组
        Map<Long, List<Comment>> repliesMap = allReplies.stream()
            .collect(Collectors.groupingBy(Comment::getRootId));
        
        // 关联用户信息并绑定到一级评论
        firstLevelComments.forEach(comment -> {
            List<Comment> replies = repliesMap.getOrDefault(comment.getId(), new ArrayList<>());
            enrichUserInfo(replies); // 填充用户信息
            comment.setReplies(replies);
        });
    }
    
    // 4. 构建VO返回
    return new PostVO(post, firstLevelComments);
}

// 填充评论中的用户信息
private void enrichUserInfo(List<Comment> comments) {
    Set<Long> userIds = comments.stream()
        .flatMap(c -> Stream.of(c.getUserId(), c.getParentId()))
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
    
    Map<Long, String> userMap = userService.getUsernames(userIds);
    
    comments.forEach(c -> {
        c.setUsername(userMap.get(c.getUserId()));
        if (c.getParentId() != null) {
            c.setParentUsername(userMap.get(c.getParentId()));
        }
    });
}

3. 楼中楼分页查询

public PageResult<Comment> getPagedReplies(Long rootId, int page, int size) {
    Page<Comment> paged = new Page<>(page, size);
    QueryWrapper<Comment> wrapper = new QueryWrapper<Comment>()
        .eq("root_id", rootId)
        .orderByAsc("create_time");
    
    IPage<Comment> result = commentMapper.selectPage(paged, wrapper);
    enrichUserInfo(result.getRecords()); // 填充用户信息
    
    return new PageResult<>(
        result.getRecords(),
        result.getTotal(),
        result.getCurrent(),
        result.getSize()
    );
}

前端交互实现

1. 楼中楼展示逻辑

// 伪代码:渲染评论树
function renderComment(comment) {
  return `
    <div class="comment">
      <span>${comment.username}: ${comment.content}</span>
      ${comment.replies.slice(0, 2).map(reply => `
        <div class="reply">
          ${reply.parentUsername 
            ? `${reply.username} 回复 @${reply.parentUsername}:` 
            : reply.username}
          ${reply.content}
        </div>
      `)}
      ${comment.replies.length > 2 && `
        <button onclick="loadAllReplies(${comment.id})">
          展开全部${comment.replies.length}条回复
        </button>
      `}
    </div>
  `;
}

// 加载完整楼中楼
async function loadAllReplies(rootId) {
  const { data } = await api.get(`/comments/${rootId}/replies?page=1&size=100`);
  // 渲染完整回复列表
}

2. 发表评论API调用

// 发表一级评论
function postFirstLevel() {
  api.post('/comments', {
    postId: 123,
    content: 'Java真棒!',
    parentId: null
  });
}

// 回复评论
function replyComment(parentId) {
  api.post('/comments', {
    postId: 123,
    content: '我赞同!',
    parentId: parentId
  });
}

关键优化点

  1. 高效查询设计

    • root_id索引优化楼中楼查询
    • 批量用户信息查询减少数据库压力
    • 一级评论与楼中楼分离加载
  2. 数据一致性

    @Transactional
    public void addComment(Comment comment) {
        // 插入评论和更新root_id在同一个事务
    }
    
  3. 前端体验优化

    • 默认展示2条楼中楼
    • 分页加载大量楼中楼
    • 实时显示新评论(WebSocket可选)
  4. 安全防护

    // 评论内容校验
    @NotBlank(message = "内容不能为空")
    @Size(max = 500, message = "评论过长")
    private String content;
    

接口设计示例

端点方法说明
/posts/{id}GET获取帖子+分层评论
/commentsPOST发表新评论
/comments/{rootId}/repliesGET获取楼中楼(分页)

性能考量

  1. 百万级评论优化方案:

    • 添加create_time索引加速排序
    • 使用Elasticsearch实现评论搜索
    • 二级评论采用游标分页
    SELECT * FROM comment 
    WHERE root_id = #{rootId} AND create_time > #{lastTime}
    ORDER BY create_time ASC
    LIMIT 20
    
  2. 缓存策略:

    @Cacheable(value = "posts", key = "#postId")
    public PostVO getPostDetail(Long postId) {
        // ...
    }
    

此方案完整实现了社区发帖、二级评论、楼中楼展开等功能,兼顾了功能实现和系统性能,可根据实际需求扩展用户认证、敏感词过滤等模块。