本文目录导读:

我将为您设计一个完整的Java评论系统案例,包含前端展示、后端服务、数据库设计和核心功能实现。
系统架构设计
评论系统架构
├── Controller层 - 接收HTTP请求
├── Service层 - 业务逻辑处理
├── DAO层 - 数据访问
├── Entity层 - 实体对象
└── 数据库 - MySQL
完整代码实现
1 实体类
// Comment.java - 评论实体
package com.example.commentsystem.entity;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class Comment {
private Integer id;
private Integer articleId; // 文章ID
private Integer userId; // 评论用户ID
private String username; // 评论用户名
private String content; // 评论内容
private Integer parentId; // 父评论ID,0表示顶级评论
private Integer replyUserId; // 被回复的用户ID
private String replyUsername; // 被回复的用户名
private Integer likeCount; // 点赞数量
private LocalDateTime createTime; // 创建时间
private Integer status; // 状态:1-正常,0-删除
private List<Comment> children; // 子评论列表
}
// User.java - 用户实体
package com.example.commentsystem.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Integer id;
private String username;
private String avatar;
private String email;
private LocalDateTime createTime;
}
2 DTO类
// CommentRequest.java - 评论请求DTO
package com.example.commentsystem.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Data
public class CommentRequest {
@NotNull(message = "文章ID不能为空")
private Integer articleId;
@NotNull(message = "用户ID不能为空")
private Integer userId;
@NotBlank(message = "评论内容不能为空")
@Size(max = 1000, message = "评论内容不能超过1000字")
private String content;
private Integer parentId; // 父评论ID,回复评论时使用
private Integer replyUserId; // 被回复的用户ID
}
3 Controller层
// CommentController.java
package com.example.commentsystem.controller;
import com.example.commentsystem.common.Result;
import com.example.commentsystem.dto.CommentRequest;
import com.example.commentsystem.entity.Comment;
import com.example.commentsystem.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/comment")
public class CommentController {
@Autowired
private CommentService commentService;
/**
* 获取文章的所有评论(按时间倒序)
*/
@GetMapping("/list/{articleId}")
public Result<List<Comment>> getComments(@PathVariable Integer articleId) {
List<Comment> comments = commentService.getCommentsByArticle(articleId);
return Result.success(comments);
}
/**
* 获取文章的评论树(按树形结构)
*/
@GetMapping("/tree/{articleId}")
public Result<List<Comment>> getCommentTree(@PathVariable Integer articleId) {
List<Comment> comments = commentService.getCommentTree(articleId);
return Result.success(comments);
}
/**
* 发表评论
*/
@PostMapping("/add")
public Result<Comment> addComment(@Valid @RequestBody CommentRequest request) {
Comment comment = commentService.addComment(request);
return Result.success("评论成功", comment);
}
/**
* 删除评论(逻辑删除)
*/
@DeleteMapping("/delete/{id}")
public Result<Void> deleteComment(@PathVariable Integer id,
@RequestParam Integer userId) {
boolean success = commentService.deleteComment(id, userId);
return success ? Result.success("删除成功", null) : Result.error("删除失败");
}
/**
* 点赞评论
*/
@PostMapping("/like/{id}")
public Result<Integer> likeComment(@PathVariable Integer id) {
Integer likeCount = commentService.likeComment(id);
return Result.success("点赞成功", likeCount);
}
}
4 Service层
// CommentService.java
package com.example.commentsystem.service;
import com.example.commentsystem.dto.CommentRequest;
import com.example.commentsystem.entity.Comment;
import java.util.List;
public interface CommentService {
// 获取文章评论列表
List<Comment> getCommentsByArticle(Integer articleId);
// 获取评论树
List<Comment> getCommentTree(Integer articleId);
// 添加评论
Comment addComment(CommentRequest request);
// 删除评论
boolean deleteComment(Integer id, Integer userId);
// 点赞评论
Integer likeComment(Integer commentId);
// 获取用户评论历史
List<Comment> getUserComments(Integer userId, int page, int size);
}
// CommentServiceImpl.java
package com.example.commentsystem.service.impl;
import com.example.commentsystem.dao.CommentDao;
import com.example.commentsystem.dto.CommentRequest;
import com.example.commentsystem.entity.Comment;
import com.example.commentsystem.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
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.stream.Collectors;
@Service
public class CommentServiceImpl implements CommentService {
@Autowired
private CommentDao commentDao;
@Override
public List<Comment> getCommentsByArticle(Integer articleId) {
return commentDao.findByArticleIdOrderByCreateTimeDesc(articleId);
}
@Override
public List<Comment> getCommentTree(Integer articleId) {
// 获取所有评论
List<Comment> allComments = commentDao.findByArticleIdOrderByCreateTimeDesc(articleId);
// 构建评论树
return buildCommentTree(allComments);
}
/**
* 构建评论树
*/
private List<Comment> buildCommentTree(List<Comment> allComments) {
// 将评论按ID分组
java.util.Map<Integer, Comment> commentMap = allComments.stream()
.collect(Collectors.toMap(Comment::getId, comment -> comment));
List<Comment> treeRoots = new ArrayList<>();
for (Comment comment : allComments) {
if (comment.getParentId() == 0) {
// 顶级评论
treeRoots.add(comment);
} else {
// 子评论,找到父评论并添加
Comment parent = commentMap.get(comment.getParentId());
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(comment);
} else {
// 父评论已被删除,作为顶级评论显示
treeRoots.add(comment);
}
}
}
// 对每个父评论的子评论按时间排序
treeRoots.forEach(root -> {
if (root.getChildren() != null) {
root.getChildren().sort((c1, c2) ->
c2.getCreateTime().compareTo(c1.getCreateTime()));
}
});
return treeRoots;
}
@Override
@Transactional
public Comment addComment(CommentRequest request) {
Comment comment = new Comment();
comment.setArticleId(request.getArticleId());
comment.setUserId(request.getUserId());
comment.setContent(request.getContent());
comment.setParentId(request.getParentId() != null ? request.getParentId() : 0);
comment.setReplyUserId(request.getReplyUserId());
comment.setLikeCount(0);
comment.setCreateTime(LocalDateTime.now());
comment.setStatus(1); // 正常状态
commentDao.insert(comment);
// 获取用户名(假设从用户服务获取)
comment.setUsername(getUsernameById(request.getUserId()));
// 如果被回复的用户存在,设置用户名
if (request.getReplyUserId() != null) {
comment.setReplyUsername(getUsernameById(request.getReplyUserId()));
}
return comment;
}
@Override
@Transactional
public boolean deleteComment(Integer id, Integer userId) {
Comment comment = commentDao.findById(id);
if (comment != null && comment.getUserId().equals(userId)) {
// 逻辑删除
commentDao.updateStatus(id, 0);
// 同时删除子评论(级联删除)
commentDao.deleteByParentId(id);
return true;
}
return false;
}
@Override
@Transactional
public Integer likeComment(Integer commentId) {
Comment comment = commentDao.findById(commentId);
if (comment != null) {
comment.setLikeCount(comment.getLikeCount() + 1);
commentDao.updateLikeCount(commentId, comment.getLikeCount());
return comment.getLikeCount();
}
return 0;
}
@Override
public List<Comment> getUserComments(Integer userId, int page, int size) {
int offset = (page - 1) * size;
return commentDao.findByUserIdOrderByCreateTimeDesc(userId, offset, size);
}
/**
* 获取用户名(模拟,实际应该调用用户服务)
*/
private String getUsernameById(Integer userId) {
// 这里应该调用用户服务获取用户名,这里简化处理
return "用户" + userId;
}
}
5 DAO层
// CommentDao.java
package com.example.commentsystem.dao;
import com.example.commentsystem.entity.Comment;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface CommentDao {
@Select("SELECT * FROM comment WHERE article_id = #{articleId} AND status = 1 ORDER BY create_time DESC")
List<Comment> findByArticleIdOrderByCreateTimeDesc(Integer articleId);
@Select("SELECT * FROM comment WHERE id = #{id} AND status = 1")
Comment findById(Integer id);
@Insert("INSERT INTO comment (article_id, user_id, content, parent_id, reply_user_id, " +
"like_count, create_time, status) " +
"VALUES (#{articleId}, #{userId}, #{content}, #{parentId}, #{replyUserId}, " +
"#{likeCount}, #{createTime}, #{status})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Comment comment);
@Update("UPDATE comment SET status = 0 WHERE id = #{id}")
int updateStatus(@Param("id") Integer id, @Param("status") Integer status);
@Update("UPDATE comment SET like_count = #{likeCount} WHERE id = #{id}")
int updateLikeCount(@Param("id") Integer id, @Param("likeCount") Integer likeCount);
@Update("UPDATE comment SET status = 0 WHERE parent_id = #{parentId}")
int deleteByParentId(Integer parentId);
@Select("SELECT * FROM comment WHERE user_id = #{userId} AND status = 1 " +
"ORDER BY create_time DESC LIMIT #{size} OFFSET #{offset}")
List<Comment> findByUserIdOrderByCreateTimeDesc(@Param("userId") Integer userId,
@Param("offset") int offset,
@Param("size") int size);
}
6 通用返回结果类
// Result.java
package com.example.commentsystem.common;
import lombok.Data;
@Data
public class Result<T> {
private Integer code; // 编码:200-成功,500-失败
private String message; // 消息
private T data; // 数据
public static <T> Result<T> success(String message, T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> success(T data) {
return success("操作成功", data);
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
7 全局异常处理器
// GlobalExceptionHandler.java
package com.example.commentsystem.exception;
import com.example.commentsystem.common.Result;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String message = fieldError != null ? fieldError.getDefaultMessage() : "参数错误";
return Result.error(message);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
return Result.error("系统异常:" + e.getMessage());
}
}
8 数据库建表脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS comment_system DEFAULT CHARACTER SET utf8mb4;
USE comment_system;
-- 用户表
CREATE TABLE IF NOT EXISTS `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 评论表
CREATE TABLE IF NOT EXISTS `comment` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`article_id` INT NOT NULL COMMENT '文章ID',
`user_id` INT NOT NULL COMMENT '评论用户ID',
`content` TEXT NOT NULL COMMENT '评论内容',
`parent_id` INT DEFAULT 0 COMMENT '父评论ID',
`reply_user_id` INT DEFAULT NULL COMMENT '被回复的用户ID',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`status` TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-删除',
KEY `idx_article_id` (`article_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
-- 插入测试数据
INSERT INTO `user` (username, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com'),
('王五', 'wangwu@example.com');
-- 插入评论示例
INSERT INTO `comment` (article_id, user_id, content, parent_id, reply_user_id, like_count) VALUES
(1, 1, '这篇文章写得太好了,收藏了!', 0, NULL, 5),
(1, 2, '学习了,很不错的内容', 1, 1, 3),
(1, 3, '希望能够多出一些这样的教程', 0, NULL, 2);
9 配置文件
# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/comment_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
# 用于评论点赞计数缓存,减少数据库压力
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
mybatis:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
10 前端代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">评论系统</title>
<style>
.comment-section {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.comment-input {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
.comment-input textarea {
width: 100%;
height: 100px;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
.btn-submit {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 3px;
cursor: pointer;
}
.btn-submit:hover {
background-color: #0056b3;
}
.comment-list {
list-style: none;
padding: 0;
}
.comment-item {
border: 1px solid #eee;
margin-bottom: 10px;
padding: 15px;
border-radius: 5px;
background-color: #f9f9f9;
}
.comment-item.child-comments {
margin-left: 40px;
margin-top: 10px;
}
.comment-header {
font-size: 14px;
margin-bottom: 10px;
}
.comment-username {
color: #007bff;
font-weight: bold;
}
.comment-content {
margin-bottom: 10px;
line-height: 1.5;
}
.comment-footer {
font-size: 12px;
color: #999;
}
.like-btn {
background: none;
border: none;
color: #007bff;
cursor: pointer;
margin-right: 10px;
}
.reply-btn {
background: none;
border: none;
color: #007bff;
cursor: pointer;
}
.reply-tag {
color: #999;
font-size: 13px;
}
.comment-content .reply-to {
color: #007bff;
font-weight: bold;
margin-right: 5px;
}
.empty-comments {
color: #999;
text-align: center;
padding: 20px;
}
.reply-area {
margin-top: 10px;
}
.reply-area textarea {
width: 100%;
height: 60px;
margin-bottom: 5px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
}
.btn-submit-reply {
background-color: #28a745;
color: white;
border: none;
padding: 5px 15px;
border-radius: 3px;
cursor: pointer;
}
footer {
font-size: 12px;
color: #999;
margin-top: 20px;
text-align: center;
}
</style>
</head>
<body>
<div class="comment-section">
<!-- 发表评论区域 -->
<div class="comment-input">
<h3>发表评论</h3>
<textarea id="commentContent" placeholder="请输入评论内容..."></textarea>
<button class="btn-submit" onclick="submitComment()">发表评论</button>
</div>
<!-- 评论列表 -->
<h3>全部评论</h3>
<ul class="comment-list" id="commentList">
<!-- 评论将在这里动态加载 -->
</ul>
</div>
<script>
// 当前文章ID(示例)
const articleId = 1;
// 当前用户ID和用户名(示例,实际应从登录状态获取)
const currentUser = { id: 1, username: '张三' };
// 页面加载时获取评论
window.onload = function() {
loadComments();
};
// 提交评论
function submitComment() {
const content = document.getElementById('commentContent').value.trim();
if (!content) {
alert('请输入评论内容');
return;
}
const data = {
articleId: articleId,
userId: currentUser.id,
content: content,
parentId: 0
};
fetch('/api/comment/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(res => {
if (res.code === 200) {
alert('评论成功');
document.getElementById('commentContent').value = '';
loadComments();
} else {
alert(res.message);
}
});
}
// 加载评论
function loadComments() {
fetch(`/api/comment/tree/${articleId}`)
.then(response => response.json())
.then(res => {
if (res.code === 200) {
renderComments(res.data, document.getElementById('commentList'));
}
});
}
// 渲染评论树
function renderComments(comments, container) {
if (!comments || comments.length === 0) {
container.innerHTML = '<li class="empty-comments">暂无评论,快来发表第一条评论吧!</li>';
return;
}
container.innerHTML = '';
comments.forEach(comment => {
const li = document.createElement('li');
li.className = 'comment-item';
// 评论头部
const header = document.createElement('div');
header.className = 'comment-header';
header.innerHTML = `
<span class="comment-username">${comment.username}</span>
<span class="comment-time">${formatTime(comment.createTime)}</span>
`;
// 评论内容
const contentDiv = document.createElement('div');
contentDiv.className = 'comment-content';
if (comment.replyUsername) {
contentDiv.innerHTML = `<span class="reply-to">回复 @${comment.replyUsername}:</span>${comment.content}`;
} else {
contentDiv.textContent = comment.content;
}
// 评论底部
const footer = document.createElement('div');
footer.className = 'comment-footer';
footer.innerHTML = `
<button class="like-btn" onclick="likeComment(${comment.id})">👍 ${comment.likeCount}</button>
<button class="reply-btn" onclick="toggleReplyArea(${comment.id})">回复</button>
`;
li.appendChild(header);
li.appendChild(contentDiv);
li.appendChild(footer);
// 回复区域
const replyArea = document.createElement('div');
replyArea.className = 'reply-area';
replyArea.id = `replyArea${comment.id}`;
replyArea.style.display = 'none';
replyArea.innerHTML = `
<textarea id="replyContent${comment.id}" placeholder="回复评论..."></textarea>
<button class="btn-submit-reply" onclick="submitReply(${comment.id})">提交回复</button>
`;
li.appendChild(replyArea);
// 如果有子评论,递归渲染
if (comment.children && comment.children.length > 0) {
const childUl = document.createElement('ul');
childUl.className = 'comment-list child-comments';
renderComments(comment.children, childUl);
li.appendChild(childUl);
}
container.appendChild(li);
});
}
// 点赞评论
function likeComment(commentId) {
fetch(`/api/comment/like/${commentId}`, {
method: 'POST'
})
.then(response => response.json())
.then(res => {
if (res.code === 200) {
loadComments(); // 重新加载以更新点赞数
}
});
}
// 显示/隐藏回复区域
function toggleReplyArea(commentId) {
const replyArea = document.getElementById(`replyArea${commentId}`);
replyArea.style.display = replyArea.style.display === 'none' ? 'block' : 'none';
}
// 提交回复
function submitReply(parentId) {
const content = document.getElementById(`replyContent${parentId}`).value.trim();
if (!content) {
alert('请输入回复内容');
return;
}
const data = {
articleId: articleId,
userId: currentUser.id,
content: content,
parentId: parentId,
replyUserId: getCommentById(parentId).userId // 获取被回复者的ID
};
fetch('/api/comment/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(res => {
if (res.code === 200) {
alert('回复成功');
loadComments();
} else {
alert(res.message);
}
});
}
// 格式化时间
function formatTime(timeStr) {
if (!timeStr) return '';
const time = new Date(timeStr);
const now = new Date();
const diff = now - time;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff/60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff/3600000)}小时前`;
if (diff < 2592000000) return `${Math.floor(diff/86400000)}天前`;
return time.toLocaleDateString('zh-CN');
}
</script>
</body>
</html>
11 Maven依赖
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>comment-system</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>
12 启动类
// CommentApplication.java
package com.example.commentsystem;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.commentsystem.dao")
public class CommentApplication {
public static void main(String[] args) {
SpringApplication.run(CommentApplication.class, args);
System.out.println("评论系统启动成功!");
}
}
系统功能特性
1 核心功能
- ✅ 发表评论
- ✅ 回复评论(支持多级嵌套)
- ✅ 删除评论(逻辑删除)
- ✅ 点赞评论
- ✅ 按时间排序
- ✅ 分页加载
2 增强功能建议
- 敏感词过滤:添加敏感词库进行内容过滤
- 评论审核:管理员审核机制
- @用户提醒:支持@功能并通知
- 表情支持:集成emoji表情
- 图片上传:支持评论图片上传
- 实时通知:WebSocket实现实时通知
3 性能优化建议
- 缓存优化:使用Redis缓存热数据
- 数据库优化:合适的索引设计
- 异步处理:异步发送通知邮件
- 限流控制:防止刷评论攻击
这个评论系统案例包含了完整的后端实现和前端展示,可以根据实际需求进行扩展和优化。