场景描述: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
});
}
关键优化点
-
高效查询设计:
root_id索引优化楼中楼查询- 批量用户信息查询减少数据库压力
- 一级评论与楼中楼分离加载
-
数据一致性:
@Transactional public void addComment(Comment comment) { // 插入评论和更新root_id在同一个事务 } -
前端体验优化:
- 默认展示2条楼中楼
- 分页加载大量楼中楼
- 实时显示新评论(WebSocket可选)
-
安全防护:
// 评论内容校验 @NotBlank(message = "内容不能为空") @Size(max = 500, message = "评论过长") private String content;
接口设计示例
| 端点 | 方法 | 说明 |
|---|---|---|
| /posts/{id} | GET | 获取帖子+分层评论 |
| /comments | POST | 发表新评论 |
| /comments/{rootId}/replies | GET | 获取楼中楼(分页) |
性能考量
-
百万级评论优化方案:
- 添加
create_time索引加速排序 - 使用Elasticsearch实现评论搜索
- 二级评论采用游标分页
SELECT * FROM comment WHERE root_id = #{rootId} AND create_time > #{lastTime} ORDER BY create_time ASC LIMIT 20 - 添加
-
缓存策略:
@Cacheable(value = "posts", key = "#postId") public PostVO getPostDetail(Long postId) { // ... }
此方案完整实现了社区发帖、二级评论、楼中楼展开等功能,兼顾了功能实现和系统性能,可根据实际需求扩展用户认证、敏感词过滤等模块。