1.显示评论
- 数据层:根据实体查询一页评论数据、根据实体查询评论的数量
- 业务层:处理查询评论的业务、处理查询评论数量的业务
- 表现层:显示帖子详情数据时,同时显示该帖子所有的评论数据
1.1 数据访问层
- entity_type:实体类型(评论目标类别,例如 1 代表帖子、2 代表评论、3代表用户)
- entity_id:类型目标(id 为 1、id为 2 的帖子)
- target_id:指向某个评论的回复目标
创建表的实体类型,在 entity 包下创建 Comment 的实体类:
package com.example.demo.entity;
import java.util.Date;
public class Comment {
private int id;
private int userId;
private int entityType;
private int entityId;
private int targetId;
private String content;
private int status;
private Date createTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getEntityType() {
return entityType;
}
public void setEntityType(int entityType) {
this.entityType = entityType;
}
public int getEntityId() {
return entityId;
}
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public int getTargetId() {
return targetId;
}
public void setTargetId(int targetId) {
this.targetId = targetId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Comment{" +
"id=" + id +
", userId=" + userId +
", entityType=" + entityType +
", entityId=" + entityId +
", targetId=" + targetId +
", content='" + content + '\'' +
", status=" + status +
", createTime=" + createTime +
'}';
}
}
开发实体组件:在 dao 包下新建一个接口 CommentMapper :
- 查询评论需要支持分页查询,需要提供两个方法:查询某一页有多少个数据、一共有多少数据(知道总页数)
package com.example.demo.dao;
import com.example.demo.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CommentMapper {
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
int selectCountByEntity(int entityType, int entityId);
}
在 resources 资源文件下 mapper 包下创建 discusspost-mapper.xml 写实现方法 :
- 定义查询字段
- 实现上述两个方法的语句
<?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.example.demo.dao.CommentMapper">
<!--定义查询字段-->
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentsByEntity" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time asc
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
</mapper>
1.2 业务层
在 service 包下新建业务组件 CommentService 类:
- 注入 CommentMapper
- 提供两个业务方法
- 1.查询某一页数据,返回集合:实体类型、类型目标、分页、每一页数量
- 2.一共有多少数据
package com.example.demo.service;
import com.example.demo.dao.CommentMapper;
import com.example.demo.entity.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
public int findCommentCount(int entityType, int entityId) {
return commentMapper.selectCountByEntity(entityType, entityId);
}
}
1.3 表现层
在 controller 包下的 DiscussPostController 类( 查询帖子详情数据的方法)中 查询请求方法 补充:
- 注入 CommentService
- 查询不单是查询帖子数据,还要查询评论(支持分页,需要传入分页相关信息的 Page)
- 补充评论分页信息:显示分页条数、显示路径、总的数据
- 分页查询,调用 commentService,首先查询帖子数据(帖子实体类型、id、分页条件)得到一个集合(得到当前帖子的所有评论)
- 将帖子实体类型写到 常量接口中(util 包下的 CommunityConstant 类中)
/**
* 实体类型: 帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型: 评论
*/
int ENTITY_TYPE_COMMENT = 2;
- 此时 DiscussPostService 类中实现常量接口(implements CommunityConstant)
- 补充帖子内容(将评论id、tardet_id 等信息转化为 User 对象得到 UserName,头像等内容):创建一个集合,对展示的数据进行统一封装
- VoList——显示对象
- 判断集合,如果集合非空,遍历集合,每次遍历得到一个评论,创建一个 Map,用来封装呈现给页面数据(也就是一个评论,将遍历的评论存入、将评论的作者存入),上述是关于帖子的评论
- 但是评论也有评论,也就是回复:遍历的同时也要将 “回复列表” 查询到返回给页面(从评论中查找评论,目标是评论的评论)
- 同理回复列表也需要创建一个集合对数据封装,然后对这个集合进行判断
- 每次遍历得到一个回复,创建一个 Map,用来封装呈现给页面数据(也就是一个回复,将遍历的回复存入、将回复的作者存入)
- 评论是没有回复目标的,但是评论的评论也就是回复,才会有回复目标(给那个帖子回复),所以给回复添加回复目标
- 最后将回复 Map 装入集合中
- 跳到 “回复” 外的循环,将回复集合装入评论map中
- 还要补充回复数量,存入评论中
- 将评论装入评论集合中
- 整个循环外,将最终结果放入 Model 中
@Autowired
private CommentService commentService;、
//查询请求方法
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
//第一种方法:在查询的时候使用关联查询 。优点:查询快;缺点:可能存在冗余、耦合
//第二种方法:先查出帖子数据,根据 id 调用 Userservice 查询 User,再通过 Model 将 User 发送给 模板,
// 这样模板得到了帖子,也得到了模板。优点:查询两次,没有冗余;缺点:查询慢
//在这里使用第二种情况,查询慢可以使用 Redis 来优化
// 作者
User user = userService.findUserById(post.getUserId());
// 把作者传给模板
model.addAttribute("user", user);
//评论分页信息
page.setLimit(5);//每页5个评论
page.setPath("discuss/detail/" + discussPostId);//访问路径
page.setRows(post.getCommentCount());//总数
// 评论: 给帖子的评论
// 回复: 给评论的评论
// 评论列表——发布帖子的评论列表
//分页查询,调用 commentService,首先查询帖子数据(帖子实体类型、id、分页条件)得到一个集合(得到当前帖子的所有评论)
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
//评论VO列表——创建一个集合,对展示的数据进行统一封装
List<Map<String, Object>> commentVoList = new ArrayList<>();
//判断集合,如果集合非空,遍历集合,每次遍历得到一个评论
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO:创建一个 Map,用来封装呈现给页面数据(也就是一个评论,将遍历的评论存入、将评论的作者存入)
Map<String, Object> commentVo = new HashMap<>();
// 评论存入
commentVo.put("comment", comment);
// 作者存入
commentVo.put("user", userService.findUserById(comment.getUserId()));
//但是评论也有评论,也就是回复:遍历的同时也要将 “回复列表” 查询到返回给页面(从评论中查找评论,目标是评论的评论)
//回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(),0, Integer.MAX_VALUE);
//回复Vo列表:创建一个集合对数据封装
List<Map<String, Object>> replyVoList = new ArrayList<>();
//每次遍历得到一个回复,创建一个 Map,用来封装呈现给页面数据(也就是一个回复,将遍历的回复存入、将回复的作者存入)
if (replyList != null) {
for (Comment reply: replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
//评论是没有回复目标的,但是评论的评论也就是回复,才会有回复目标(给那个帖子回复),所以给回复添加回复目标
// 回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
//最后将回复 Map 装入集合中
replyVoList.add(replyVo);
}
}
//将回复集合装入评论map中
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
//存入评论中
commentVo.put("replyCount", replyCount);
//将评论装入评论集合中
commentVoList.add(commentVo);
}
}
//将最终结果放入 Model 中
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
2.添加评论
- 数据层:增加评论数量、修改帖子的评论数量
- 业务层:处理添加评论的业务、先增加评论、再更新帖子的评论数量
- 表现层:处理添加评论数量的请求、设置添加评论的表单
2.1 数据层
在 dao 包下的 CommentMapper 中新增 添加评论 的方法:
//添加评论
int insertComment(Comment comment);
在 配置文件 comment-mapper.xml 中实现 sql:
- 列出新增字段
- 添加 insert 标签
<!--添加评论查询字段-->
<sql id="insertFields">
user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<!--添加评论sql-->
<insert id="insertComment" parameterType="Comment">
insert into comment(<include refid="insertFields"></include>)
values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>
增加评论之后更新帖子数量,在 DiscussPostMapper.java 中新增方法:
- 根据 id 更新帖子数量
//更新帖子数量
int updateCommentCount(int id, int commentCount);
打开接口的配置文件 discusspost-mapper.xml 实现 sql:
<!--更新帖子数量-->
<update id="updateCommentCount">
update discuss_post set comment_count = #{commentCount} where id = #{id}
</update>
2.2 业务层
在 service 包下的 DiscussPostService(帖子业务组件)中增加更新评论数量:
//更新评论数量
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
在 CommentService(显示评论的业务方法) 中处理增加评论业务:
- 增加评论返回 int,参数为实体 Comment
- 添加注解 @Transactional (声明隔离级别、传播机制):因为在增加评论方法中有两次 DMA 操作,进行事务管理,要么一起成功,要么一起失败
- 增加评论的时候,需要过滤内容(标签、敏感词等),注入 SensitiveFilter(敏感词)
- 调用 增加评论的方法
- 在更新帖子评论数量:判断实体类型是否是帖子,实现 CommunityConstant 接口,在查找评论数量;最后更新到帖子表中(根据帖子 id更新),需要注入 DiscussPostService
@Autowired
private SensitiveFilter sensitiveFilter;
@Autowired
private DiscussPostService discussPostService;
//增加评论的方法
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if (comment == null) {
throw new IllegalArgumentException("参数不能为空");
}
//添加评论:过滤标签和敏感词
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(comment.getContent()));
int rows = commentMapper.insertComment(comment);
//更新帖子评论数量
//判断实体类型是否是帖子,实现 CommunityConstant 接口,在查找评论数量;
//最后更新到帖子表中(根据帖子 id更新),需要注入 DiscussPostService
if (comment.getEntityType() == ENTITY_TYPE_POST) {
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
2.3 表现层
在 controller 包下新建 CommentController (处理增加评论的请求)类
- 声明 controller 访问路径
- 增加请求路径(添加帖子 id 路径),增加是POST 请求
- 通过 @PathVariable 得到帖子 id 参数,(传入提交内容、id、评论类型,声明实体类 Comment)
- 注入 CommentService,HostHolder(增加评论为当前用户增加);
- 补充实体类:UserId、状态、当前时间
- 添加评论
- 最终重定向帖子详情页
package com.example.demo.controller;
import com.example.demo.entity.Comment;
import com.example.demo.service.CommentService;
import com.example.demo.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Date;
/**
* 处理增加评论的请求
*/
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
//通过 @PathVariable 得到帖子 id 参数,(传入提交内容、id、评论类型,声明实体类 Comment)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
return "redirect:/discuss/detail/" + discussPostId;
}
}
前端页面 discuss-detail.html 中的回帖:
<!-- 回帖 -->
<div class="container mt-3">
<!-- 回帖数量 -->
<div class="row">
<div class="col-8">
<h6><b class="square"></b> <i th:text="${post.commentCount}">30</i>条回帖</h6>
</div>
<div class="col-4 text-right">
<a href="#replyform" class="btn btn-primary btn-sm"> 回 帖 </a>
</div>
</div>
<!-- 回帖列表 -->
<ul class="list-unstyled mt-4">
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="cvo:${comments}">
<a href="profile.html">
<img th:src="${cvo.user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="media-body">
<div class="mt-0">
<span class="font-size-12 text-success" th:utext="${cvo.user.username}">掉脑袋切切</span>
<span class="badge badge-secondary float-right floor">
<i th:text="${page.offset + cvoStat.count}">1</i>#
</span>
</div>
<div class="mt-2" th:utext="${cvo.comment.content}">
这开课时间是不是有点晚啊。。。
</div>
<div class="mt-4 text-muted font-size-12">
<span>发布于 <b th:text="${#dates.format(cvo.comment.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b></span>
<ul class="d-inline float-right">
<li class="d-inline ml-2"><a href="#" class="text-primary">赞(1)</a></li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2"><a href="#" class="text-primary">回复(<i th:text="${cvo.replyCount}">2</i>)</a></li>
</ul>
</div>
<!-- 回复列表 -->
<ul class="list-unstyled mt-4 bg-gray p-3 font-size-12 text-muted">
<li class="pb-3 pt-3 mb-3 border-bottom" th:each="rvo:${cvo.replys}">
<div>
<span th:if="${rvo.target==null}">
<b class="text-info" th:text="${rvo.user.username}">寒江雪</b>:
</span>
<span th:if="${rvo.target!=null}">
<i class="text-info" th:text="${rvo.user.username}">Sissi</i> 回复
<b class="text-info" th:text="${rvo.target.username}">寒江雪</b>:
</span>
<span th:utext="${rvo.reply.content}">这个是直播时间哈,觉得晚的话可以直接看之前的完整录播的~</span>
</div>
<div class="mt-3">
<span th:text="${#dates.format(rvo.reply.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</span>
<ul class="d-inline float-right">
<li class="d-inline ml-2"><a href="#" class="text-primary">赞(1)</a></li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2"><a th:href="|#huifu-${rvoStat.count}|" data-toggle="collapse" class="text-primary">回复</a></li>
</ul>
<div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" class="input-size" name="content" th:placeholder="|回复${rvo.user.username}|"/>
<input type="hidden" name="entityType" value="2">
<input type="hidden" name="entityId" th:value="${cvo.comment.id}">
<input type="hidden" name="targetId" th:value="${rvo.user.id}">
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button>
</div>
</form>
</div>
</div>
</li>
<!-- 回复输入框 -->
<li class="pb-3 pt-3">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" class="input-size" name="content" placeholder="请输入你的观点"/>
<input type="hidden" name="entityType" value="2">
<input type="hidden" name="entityId" th:value="${cvo.comment.id}">
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm" onclick="#"> 回 复 </button>
</div>
</form>
</li>
</ul>
</div>
</li>
</ul>
<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">
<ul class="pagination justify-content-center">
<li class="page-item"><a class="page-link" href="#">首页</a></li>
<li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">4</a></li>
<li class="page-item"><a class="page-link" href="#">5</a></li>
<li class="page-item"><a class="page-link" href="#">下一页</a></li>
<li class="page-item"><a class="page-link" href="#">末页</a></li>
</ul>
</nav>
</div>
<!-- 回帖输入 -->
<div class="container mt-3">
<form class="replyform" method="post" th:action="@{|/comment/add/${post.id}|}">
<p class="mt-3">
<a name="replyform"></a>
<textarea placeholder="在这里畅所欲言你的看法吧!" name="content"></textarea>
<input type="hidden" name="entityType" value="1">
<input type="hidden" name="entityId" th:value="${post.id}">
</p>
<p class="text-right">
<button type="submit" class="btn btn-primary btn-sm"> 回 帖 </button>
</p>
</form>
</div>
</div>