0%

仿牛客论坛项目04-核心社区功能

仿牛客论坛项目04-核心社区功能,内容记录。

字典树过滤敏感词、事务管理、统一异常管理、AOP 记录日志

过滤敏感词

使用字典树(Trie树、前缀树)过滤敏感词
应用:字符串检索、词频统计、字符串排序

实现敏感词过滤器方法:

  • 定义前缀树
  • 根据敏感词,初始化前缀树
  • 编写过滤敏感词方法
    1

注意:
需要考虑敏感词之间有符号的情况。
核心代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public String filter(String text) {  
TrieNode tempNode = rootNode; // 指针1
int begin = 0; // 指针2
int position = 0; // 指针3
// 结果
StringBuilder sb = new StringBuilder();
while(position < text.length()) {
char c = text.charAt(position);

// 跳过符号
if(isSymbol(c)) {
// 若指针1处于根节点,将此符号计入结果,指针2向下走一步
if(tempNode == rootNode) {
sb.append(c);
begin ++;
}
// 无论符号在开头还是中间,指针3都向下走一步
position ++;
continue;
}

// 检查下级节点
tempNode = tempNode.getSubNode(c);
if(tempNode == null) {
// 已begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if(tempNode.isEnd()) {
// 发现敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
tempNode = rootNode;
} else {
// 继续检查下一个字符
position ++;
}
}

// 将最后一批字符计入结果,指针2疑似敏感词,指针3已经到结尾,不是敏感词
sb.append(text.substring(begin));
return sb.toString();
}

发布帖子

AJAX:

  • Asynchronous JavaScript ans XML
  • 不需要刷新整个页面,能将增量显示在页面上

示例:页面上点发送按钮,服务器返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>  
<script>
function send() {
$.post(
"/community/alpha/ajax",
{"name":"gxy", "age":23},
function (data) {
// 回调函数,服务器返回给浏览器的对象
console.log(typeof(data));
console.log(data);

data = $.parseJSON(data);
console.log(typeof(data));
console.log(data.code);
console.log(data.msg);
}
);
}
</script>

Dao 层

增加帖子的方法,并在mybatis mapper中实现数据库语句
写完需要进行测试。

Service 层

添加帖子时的处理逻辑:

  1. 参数不能为空
  2. 对 HTML 标记进行转义,方法HtmlUtils.htmlEscape()
  3. 过滤敏感词

Controller 层

页面上只需要传入标题和内容(参数)。
Controller 层逻辑:

  1. 判断用户是否登录
  2. 创建一个帖子的对象
  3. 返回 JSON 字符串

视图层

实现逻辑:

  1. 没登录时,不显示“我要发布”的按钮
  2. JS 中获取标题和内容
  3. 发送异步请求
  4. 在异步请求的回调函数中,将提示消息更新到页面

显示帖子详情

Dao、Service、Controller 层

增加查看帖子的方法

index.html

在帖子标题上增加访问详情页面的链接

1
2
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">xxx  
</a>

discuss-detail.html

  • 处理静态资源的访问路径
  • 复用 index.htmlheader 区域
  • 显示标题、作者、发布时间、帖子正文等内容

事务管理

定义: 有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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Isolation.READ_COMMITTED 作用:当前事务只能读取到已提交的数据
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if(comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}

// 转义comment
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
// 过滤敏感词
comment.setContent(sensitiveFilter.filter(comment.getContent()));
int rows = commentMapper.insertComment(comment);

// 更新帖子评论数量
if(comment.getEntityType() == ENTITY_TYPE_COMMENT) {
int count = commentMapper.selectCommentsRows(comment.getEntityType(), comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}

return rows;
}

表现层

  • 处理添加评论数据的请求
  • 设置添加评论的表单

私信列表

业务逻辑:

  • 私信列表
    • 查询当前用户的会话列表,每个会话只显示一条最新的私信
    • 支持分页显示
  • 私信详情
    • 查询某个会话所包含的私信
    • 支持分页显示

数据层

MessageMapper 有五个方法:
查询一页数据、查询数据总行数。点击一个会话进入后也要这两个方法
查询未读消息数量(针对某个会话、针对所有会话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 对于每个 conversation_id,找到 id 最大的消息(通常代表该会话中最新的消息)。
select max(id) from message
where status != 2
and from_id != 1
group by conversation_id

// 这个 IN 子查询的作用是 找到每个会话中 id 最大的消息(即最新消息),然后从 message 表中查询这些消息的完整信息。
<select id="selectConversations" resultType="Message">
select <include refid="selectField"></include>
from message
where id in (
select max(id)
from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
)
order by id desc
# 分页查询语句
limit #{offset}, #{limit}
</select>

// 这个sql是统计子查询最大id的数量
<select id="selectConversationCount" resultType="int">
select count(m.maxid) from (
select max(id) as maxid
from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
) as m
</select>

发送私信

  • 发送私信
    • 采用异步方式发送私信
    • 发送成功后刷新私信列表
  • 设置已读
    • 访问私信详情时,将私信设置为已读状态
      在用户打开私信详情页面时,如果有未读消息,就变成已读。

统一处理异常

  • SpringBoot 自动处理,将错误提示页面放在/template/error 目录下,文件名为错误码:404、500
  • 需要记录日志,用到注解
    • @ControllerAdvice:修饰类,全局统一配置。处理异常、绑定数据、绑定参数(修饰方法)
      • 处理异常:@ExceptionHandler,优点:统一处理,不用在任何一个 Controller 上处理
      • 绑定数据:@ModelAttribute
      • 绑定参数:@DataBinder

统一记录日志

不发生异常,也需要记录日志。也不能用拦截器,因为还要记录数据层、业务层。
业务层是专门处理业务的,在业务层中记录日志不专业,记录业务是系统需求,增加了耦合性。
使用 AOP 解决。

AOP 概念

Aspect Oriented Programing,面向方面(切面)编程,对 OOP 的补充。(概念抽象)
通俗理解:系统有很多业务模块,每个业务模块都有相同的需求,如记录日志,

问题

  1. 为什么要对用户输入的标题进行 HTMl 转义
    如果不转义,将用户输入的<h1><script>alert("XSS攻击成功!")</script></h1> 存进数据库中,页面加载时,JS 代码会被执行,弹出弹框。
    跳转到钓鱼网站 <a href="http://phishing.com">点击这里领奖</a>
    攻击者会注入恶意脚本,造成严重的安全漏洞。
    正确做法:在 service 层对标题等用户输入的内容进行 HTML 转义,防止安全漏洞。

  2. 说说评论功能
    评论功能主要分为两种:对帖子评论(一级评论) 和 对评论的回复(二级评论)。
    用户在帖子详情页发表评论:
    • 服务器接收评论请求,判断评论类型:
    • 如果是帖子评论,存入数据库,并更新帖子的评论数量。
    • 如果是对评论的回复,存入数据库,并记录 target_id 以标记被回复的用户。
    数据库设计:
    • comment 表通过 entity_type 和 entity_id 关联 帖子或评论
    • target_id 记录被回复用户 ID。
    事务管理:
    @Transactional 保证评论插入和帖子评论数更新是原子操作,防止数据不一致。

  3. 为了发送私信异步请求,要加上 @ResponseBody 这个注解
    在发送私信的异步请求(AJAX 请求)时,通常希望服务器返回一个 JSON 结构的响应,而不是一个 HTML 页面,因此需要加上 @ResponseBody。
    不加这个注解,默认返回的是页面。

  4. 为了提交私信时,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>