牛客后端项目(4)-帖子模块开发

核心功能开发完成~

过滤敏感词

Trie树
树的节点包括:子节点(用Map<String, TrieNode>表示),是否是单词结尾的标志(boolean isEnd)
两个关键方法:

  1. 向Trie树添加字符串
  2. 对字符串进行敏感词过滤(要注意start指针要一位一位移动,否则会漏判,除了start-end是一个敏感词,start直接跳到end+1)
  • 注意要跳过敏感词中的符号,比如❤福❤彩❤:
    CharUtils.isAsciiAlphanumeric判断字符是否是0-9a-zA-Z范围内(参考文档),以及是否是0x2e80~0x9FF之间(这是东亚文字的区间)

发布帖子

AJAX

  1. 在页面中引入jQuery:
    1
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
  2. 页面中写异步请求发送的方法,举个例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function send(){
    $.post(
    "/community/ajax",
    {"name": "liz", "age": 24},
    function (data){
    console.log(typeof data);
    console.log(data);

    data = $.parseJSON(data);
    console.log(typeof data);
    console.log(data);
    }
    );
    }

发布帖子

dao层:insert方法
service层:处理标题和内容,insert
controller层:从前端获取title和content,创建DiscussPost对象,之后调用service,返回是否成功的结果
视图:处理返回结果,发布成功时刷新,失败时留在发布界面
注意:

  1. 帖子的标题和内容需要合法性处理
  2. 帖子内容如果有html标签,要转义一下,避免内容对页面的损害。用HtmlUtils.htmlEscape方法即可

帖子详情

dao:selectById方法
service:调用dao的selectById方法
controller:postId在路径中传,调用service的select方法,找到的DiscussPost和相关的User对象,送入Model,传到前端视图中显示。

事务

事务:一组操作,要么全部执行,要么全部不执行
四个性质:ACID,原子性、一致性、隔离性、持久性
隔离性:事务与事务之间的操作是相互隔离的。主要解决并发异常问题。
常见的并发异常:1. 第一类丢失更新、第二类丢失更新;2. 脏读、不可重复度、幻读
常见的隔离级别
Read Uncommitted
Read Committed

并发异常问题与事务隔离级别

第一类丢失更新:事务A和事务B同时对同一个数据操作,事务A更新完了,但是事务B发现进行了回滚,那么对于事务A的更新就丢失了。(B回滚导致A丢失)
图片来源:牛客网课程
Img

第二类丢失更新
B提交导致A丢失
图片来源:牛客网课程
Img

脏读:事务A读了事务B还没有提交但修改了的数据

不可重复读:事务B在修改数据,事务A两次读同一个数据但读到了不一样的数据。

幻读:事务B在插入数据,事务A两次读但是读到了不一样多的数据

事务隔离级别
Y表示问题会出现,N表示问题不会出现
图片来源:牛客网课程
Img

实现机制-锁🔒

数据库自带:悲观锁(即出现并发就一定会有问题,所以一定要加锁)

  1. 共享锁(S锁):事务A对数据加了共享锁后,其他事务只能加共享锁,不能加排它锁
  2. 排它锁(X锁):事务A对数据加了排他锁后,其他事务什么锁都不能加

自定义:乐观锁(即认为出现并发不一定会有问题)
用时间戳或版本号等来实现。当要更新数据的时候,发现版本号发生变化了,就取消更新,不然就更新数据(当然也要更新一下版本号)。

扩展:MVCC到底是什么?这一篇博客就够啦_flying$的博客-CSDN
MySQL中MVCC的正确打开方式(源码佐证)_Waves___的博客-CSDN博客

Spring的事务管理

对任意数据库,有一套统一的API来使用🙆‍♀️
两种实现方法:1. 声明式,通过xml或注解配置;2.编程式,通过TransactionTemplate,再用接口回调。

@Transactinoal
isolation选择事务隔离等级
propagation选择事务传播机制,一个方法同时调用serviceA和serivceB,如果涉及事务就需要考虑A和B之间事务的关系是什么
REQUIRED:保持当前事务,如果没有则创建新事务
REQUIRES_NEW:创建新事务,并暂停当前事务
NESTED:如果存在当前事务,则嵌套在其中执行(对B会有单独的提交和回滚);没有就和REQUIRED一样

更多Propagation的解读:spring事务-说说Propagation及其实现原理_那个天真的人的博客-CSDN

显示评论

数据结构:设计一张表来保存所有评论,由于帖子、帖子的评论都可以有评论,为了减少冗余,把它们都存放在同一张表里,并用entity type来指明是帖子或评论的评论,entity id指明(帖子或评论的)id

dao:(1)用entity typeentity id查询评论列表(分页);(2)用entity typeentity id查询评论数量
service:读取某个帖子对应的所有评论,注意评论中还有对某个评论的回复。
controller:通过访问/discuss/{discussPostId}来访问所有评论,用service读,注意要分页,replyList就不用了。设计一个ViewObject返回要显示在网页上的内容,用List保存评论commentList,其中每个元素是一个map,包括一条评论所有相关的内容,即评论内容comment、发表评论的用户user(需要用户名、id和headerUrl)、该评论的回复replyList,其中replyList是和commentList相同的结构,用for循环保存起来,需要注意的是回复有target id,即回复的是哪一条评论。另外还要保存replyList的评论数量,commentList的数量可以用discussPost.commentCount来读取,省得在数据库中搜索浪费时间了。

注意:

  • 视图处理上要仔细,别的没啥。难点在于返回网页的内容处理上

问题解决

  1. controller层和service层的内容,该交给哪个层去完成感觉总是弄混。比如说这里要封装一个给视图显示的viewObject。为什么不交给Service层去处理?我的理解:Service层应具有高可复用性,如果交给service去处理的话,返回一个视图显示对象,这是不太合理的。

回复评论

dao层:插入一条评论;更新帖子的回复数量
service层:DiscussPostService更新帖子的回复数量。CommentService插入一条评论(注意要html转义、敏感词处理)、调用DiscussPostService方法更新帖子的回复数量。
视图层:controller得到comment,前端传回content、targetId、entityType、entityId,剩下的补充一下。调用service方法完成添加评论后要重定向到当前页面,从而进行刷新。

注意:

  • CommentService的插入评论方法,同时对数据库有两次修改,需要放在一个事务中处理。

私信

数据结构:message表,from_idto_id分别表示发送用户和接受用户;为了便于查询,将两者拼接为一个字符串表示会话id即conversation_id;其他还有创建时间、内容、状态。注意用户之间的会话,两者不分谁发送谁接收,这个会话是他们共有的。

私信列表

私信列表包括朋友私信和系统通知,两者逻辑可以复用。
对于朋友私信而言,列表的每一个元素是一个用户,显示该用户头像、用户名(User),以及最近一次会话的内容和时间(Message),以及所有会话数量和未读会话数量。整个列表是当前用户拥有的所有会话(无论当前用户是发送方还是接收方)
推出dao层需要的方法:

  1. UserMapper:根据id查询用户
  2. MessageMapper:(1)分页查询会话中的最近一次消息(用会话id和limit);(2)查询和某个用户之间的会话数量(用会话id和status);(3)为了分页,查询该用户的所有未删除的会话数量(用userId和status)

service:直接把dao层封装一下
controller:封装一个传给视图层的列表

注意

  1. 在统计未读消息的时候,只有对于接收方是未读的消息才要统计,对于发送方而言这条消息不是未读的!
  2. 在写sql语句的时候,因为只需要会话中的最近一次消息,用max(id)并按照conversationId分组即可

私信详情

即一个会话的多个消息。用分页展示。其他和私信列表类似。私信的信息注意用户保存的是发送方,可以提前保存起来。

发送私信

dao:insert一条message
service:内容的合法性要处理,

统一的异常处理

  1. 用户登录验证。有些方法只有在用户登录之后才可以使用(如回帖、发消息等)
  2. 不存在的网页(404)和服务器出错的网页(500)

用户登录验证

使用拦截器Interceptor,对需要登录才可以继续的方法进行拦截。为了方便,可以对这些方法统一使用相同的注释来标记(eg. LoginRequired),在拦截器中判断方法是否包含该注释,再判断用户是否已登录,如果没有登录就重定向到登录页。

404和500页面

直接把404页面放到template/error文件夹下面,spring会自动处理。如果没有使用模板引擎,就可以把404页面放到error文件夹中,error文件夹再放到static文件夹下即可。

而对于500页面,是服务器异常,我们需要把这些异常记录到日志中去,不能直接交给spring boot自动处理。
我们使用@ControllerAdvice注解定义一个新的类,从而统一扩增控制器的功能。@ControllerAdvice注解和@ExceptionHandler搭配使用,可以统一处理异常。而@ControllerAdvice中的参数可以指定需要对哪些控制器进行功能增强,如@ControllerAdvice(annotations = Controller.class)对标有Controller注解的所有类进行增强。

统一的日志记录

AOP

面向切面编程。是一种OOP编程的扩展。将项目中多个模块共有的业务看做一个切面,如下图所示(图片来源:牛客网课程
Img
Img

Joinpoint:连接点,指明aspect是在哪个位置织入。可以是属性或方法等。
Target:目标对象,Spring中就是一个Bean
Weaving(织入):有三种织入方式,分别是(1)编译时织入;(2)装载时织入;(3)运行时织入
Aspect:方面组件,我理解是在要实现的统一方法是什么(如我们要实现的就是日志这个功能)。其中由包括Pointcut和Advice,前者说明这个Aspect可以在哪里加入,后者是具体逻辑。

AOP的实现

  1. AspectJ:是一种扩展了Java的新语言;在编译时进行织入
  2. Spring AOP:用Java实现;在运行时通过动态代理进行织入;但只支持方法类型的连接点

日志功能

用Spring AOP来实现。
Spring AOP的代理有两种方式:

  1. JDK动态代理。是默认方法;如果target是接口,就用JDK动态代理来进行织入
  2. CGLib动态代理。在没有接口的情况下,就用这个方式来代理

具体实现:

  1. 类用@Component、@Aspect标注。
  2. 定义切入点:@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
  3. 定义通知逻辑。根据在织入点前后处理,可以分为Before、AfterReturning,以及Around、AfterThrowing等