仿牛客论坛项目04-核心社区功能,内容记录。
字典树过滤敏感词、事务管理、统一异常管理、AOP 记录日志
过滤敏感词
使用字典树(Trie树、前缀树)过滤敏感词
应用:字符串检索、词频统计、字符串排序
实现敏感词过滤器方法:
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词方法

注意:
需要考虑敏感词之间有符号的情况。
核心代码实现:
1 | public String filter(String text) { |
发布帖子
AJAX:
- Asynchronous JavaScript ans XML
- 不需要刷新整个页面,能将增量显示在页面上
示例:页面上点发送按钮,服务器返回数据
1 | <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script> |
Dao 层
增加帖子的方法,并在mybatis mapper中实现数据库语句
写完需要进行测试。
Service 层
添加帖子时的处理逻辑:
- 参数不能为空
- 对 HTML 标记进行转义,方法
HtmlUtils.htmlEscape() - 过滤敏感词
Controller 层
页面上只需要传入标题和内容(参数)。
Controller 层逻辑:
- 判断用户是否登录
- 创建一个帖子的对象
- 返回 JSON 字符串
视图层
实现逻辑:
- 没登录时,不显示“我要发布”的按钮
- JS 中获取标题和内容
- 发送异步请求
- 在异步请求的回调函数中,将提示消息更新到页面
显示帖子详情
Dao、Service、Controller 层
增加查看帖子的方法
index.html
在帖子标题上增加访问详情页面的链接
1 | <a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">xxx |
discuss-detail.html
- 处理静态资源的访问路径
- 复用
index.html的header区域 - 显示标题、作者、发布时间、帖子正文等内容
事务管理
定义: 有N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行
事务的特性(ACID):
- 原子性(Atomicity):事务是最小的执行体
- 一致性(Consistency):事务执行的结果,是数据从一个一致性状态,变为另一个一致性状态
- 隔离性(Isolation):各个事务内部的操作互不干扰
- 持久性(Durability):事务一旦提交,对数据做的改变都要记录到永久存储器中
事务的隔离性:
- 常见的并发异常:
第一类丢失更新(某一个事务的回滚导致另一个事务已更新的数据丢失 )、第二类丢失更新(某一个事务的提交导致另一个事务已更新的数据丢失)
脏读(某一个事务读取了另一个事务未提交的数据)、不可重复读(某一个事务,对同一个数据前后读取结果不一致)、幻读(某一个事务,对同一个表前后查询到的行数不一致) - 常见的隔离级别
Read Uncommitted:读取未提交的数据
Read Committed:读取已提交的数据
Repeatable Read:可重复读
Serializable:串行化 (会加很多锁,降低执行效率)
数据库保障事务的实现机制:
- 悲观锁(数据库自带)
- 共享锁(S锁):事务A对某数据加了共享锁后,其他事务职能加共享锁,不能加排他锁
- 排他锁(X锁):事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁
- 乐观锁(自定义)
使用版本号、时间戳等
在更新数据钱,检查版本是否发生变化,若变化则取消本次更新,否则就更新数据(版本号+1)
使用注解管理事务@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
传播方式 propagation 是一个方法调用另一个事务管理的方法,该用哪一种隔离级别,传播机制就就是用来解决这种问题,常用有三种:
REQUIRED:支持当前事务(或者说外部事务,A调用B,A是当前事务),如果不存在则创建新事务REQUIRES_NEW:创建一个新事物,并且暂停当前事务NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行(但有独立的提交和回滚),否则和REQUIRED一致
显示评论
数据层
- 根据实体查询一页评论数据
- 根据实体查询评论的数量
业务层
- 处理查询评论业务
- 处理查询评论数量的业务
表现层
- 显示帖子详情数据时,同时显示该帖子所有的评论数据
- 每个评论的回复也要同时查询评论的所有回复数据
添加评论
数据层
- 增加评论(
CommentMapper中实现) - 修改评论的数量(
discuss_post表中有个参数是评论数量,DiscussPostMapper中实现 )
业务层
- 处理增加评论业务(核心业务,在这用事务管理,要先判空,再 转义HTML,再过滤敏感词)
- 先增加评论,再更新帖子评论的数量
1 | // Isolation.READ_COMMITTED 作用:当前事务只能读取到已提交的数据 |
表现层
- 处理添加评论数据的请求
- 设置添加评论的表单
私信列表
业务逻辑:
- 私信列表
- 查询当前用户的会话列表,每个会话只显示一条最新的私信
- 支持分页显示
- 私信详情
- 查询某个会话所包含的私信
- 支持分页显示
数据层
MessageMapper 有五个方法:
查询一页数据、查询数据总行数。点击一个会话进入后也要这两个方法
查询未读消息数量(针对某个会话、针对所有会话)。
1 | // 对于每个 conversation_id,找到 id 最大的消息(通常代表该会话中最新的消息)。 |
发送私信
- 发送私信
- 采用异步方式发送私信
- 发送成功后刷新私信列表
- 设置已读
- 访问私信详情时,将私信设置为已读状态
在用户打开私信详情页面时,如果有未读消息,就变成已读。
- 访问私信详情时,将私信设置为已读状态
统一处理异常
- SpringBoot 自动处理,将错误提示页面放在
/template/error目录下,文件名为错误码:404、500 - 需要记录日志,用到注解
- @ControllerAdvice:修饰类,全局统一配置。处理异常、绑定数据、绑定参数(修饰方法)
- 处理异常:@ExceptionHandler,优点:统一处理,不用在任何一个 Controller 上处理
- 绑定数据:@ModelAttribute
- 绑定参数:@DataBinder
- @ControllerAdvice:修饰类,全局统一配置。处理异常、绑定数据、绑定参数(修饰方法)
统一记录日志
不发生异常,也需要记录日志。也不能用拦截器,因为还要记录数据层、业务层。
业务层是专门处理业务的,在业务层中记录日志不专业,记录业务是系统需求,增加了耦合性。
使用 AOP 解决。
AOP 概念
Aspect Oriented Programing,面向方面(切面)编程,对 OOP 的补充。(概念抽象)
通俗理解:系统有很多业务模块,每个业务模块都有相同的需求,如记录日志,
问题
为什么要对用户输入的标题进行 HTMl 转义
如果不转义,将用户输入的<h1><script>alert("XSS攻击成功!")</script></h1>存进数据库中,页面加载时,JS 代码会被执行,弹出弹框。
跳转到钓鱼网站<a href="http://phishing.com">点击这里领奖</a>
攻击者会注入恶意脚本,造成严重的安全漏洞。
正确做法:在service层对标题等用户输入的内容进行 HTML 转义,防止安全漏洞。说说评论功能
评论功能主要分为两种:对帖子评论(一级评论) 和 对评论的回复(二级评论)。
用户在帖子详情页发表评论:
• 服务器接收评论请求,判断评论类型:
• 如果是帖子评论,存入数据库,并更新帖子的评论数量。
• 如果是对评论的回复,存入数据库,并记录 target_id 以标记被回复的用户。
数据库设计:
• comment 表通过 entity_type 和 entity_id 关联 帖子或评论。
• target_id 记录被回复用户 ID。
事务管理:
@Transactional 保证评论插入和帖子评论数更新是原子操作,防止数据不一致。为了发送私信异步请求,要加上
@ResponseBody这个注解
在发送私信的异步请求(AJAX 请求)时,通常希望服务器返回一个 JSON 结构的响应,而不是一个 HTML 页面,因此需要加上 @ResponseBody。
不加这个注解,默认返回的是页面。为了提交私信时,status 状态是0,但发送者中是已读,接受者中是未读,明明数据库中都是一条数据?
因为数据查询语句逻辑是这样的(发送者不会把刚发送的私信当作是未读私信):1
2
3
4
5
6
7
8
9
10<select id="selectLetterUnreadCount" resultType="int">
select count(id)
from message
where status = 0
and from_id != 1
and to_id = #{userId}
<if test="conversationId != null">
and conversation_id = #{conversationId}
</if>
</select>