当前位置:   article > 正文

牛客网项目总结_牛客网上的技术项目

牛客网上的技术项目


本项目是个人独立开发的后端项目,这要涉及到Spring、SpringMVC、Mybatis的整合,SpringBoot去简化Spring的配置开发以及前后端交互渲染。
主要的技术点:

1.登录注册功能:使用kaptcha去生成验证码,使用邮件完成注册,Redis优化验证码的保存,解决分布式session问题,使用拦截器拦截用户请求,将用户信息绑定在ThreadLocal上
2.构建Trie数据结构,实现对发表帖子评论的敏感词过滤
3.利用AJAX支持对帖子评论,也支持对评论进行回复
4.利用AOP对service的业务代码实现日志记录
5.利用Redis的zset并结合Redis实现点赞关注的功能
6.点赞关注后的系统通知,实时性不需要特别高,使用kafka实现异步的发送系统通知
7.使用ElasticSearch实现对帖子的搜索功能,以及结果的高亮显示

数据库表

用户表 user
字段类型备注
idint主键,自增
usernamevarchar用户名,创建索引
passwordvarchar用户密码
saltvarchar加密盐值
emailvarchar用户邮箱,创建索引
typeint用户类型:0 普通、1 管理员、2 版主
statusint用户状态:0 未激活、1 已激活
activation_codevarchar激活码
header_urlvarchar用户头像地址
create_timetimestamp注册时间
评论表 comment
字段类型备注
idint主键、自增
user_idint评论的用户 id,创建索引
entity_idint评论实体 id,创建索引
entity_typeint评论实体类型:1 帖子评论、2 评论回复
target_idint评论目标 id
contenttext评论内容
statusint评论状态:0 有效、1 无效
create_timetimestamp评论发表时间
帖子表 discuss_post
字段类型备注
idint主键、自增
user_idint发帖的用户 id,创建索引
titlevarchar帖子表标题
contenttext帖子内容
typeint帖子类型:0 普通、1 置顶
comment_countint评论数量
statusint帖子状态:0 普通、1 精华、2 拉黑
create_timetimestamp评论发表时间
用户登录凭证表 login_ticket
字段类型备注
idint主键、自增
user_idint登录用户 id
ticketvarchar登录凭证,随机字符串
statusint登录状态:0 有效、1 无效
expiredtimestamp过期时间
消息表 message
字段类型备注
idint主键、自增
from_idint发消息的 id,创建索引
to_idint收消息的 id,创建索引
conversation_idvarchar会话 id,由通信双方 id 拼接,创建索引
contenttext消息内容
statusint消息状态:0 未读、1 已读、2 删除
create_timetimestamp消息发送时间

开发社区首页

搭建基本环境

构建 SpringBoot 的 maven 项目,引入 mysql 和 mybatis 依赖。


application.properties 配置文件中:

  • 关闭 thymeleaf 缓存
  • 配置数据库,设置基本连接信息、最大线程数,最小空闲线程数,最大空闲时间等
  • mybatis,设置 mapper 文件的位置、实体类包名、使用主键等

创建 community 数据库和数据库表。


用户相关操作:

  • 创建对应 user 表的 User 实体类
  • 创建 UserMapper 接口,使用 @Mapper 注解
  • 创建 user-mapper.xml,重复 sql 语句可以写在 <sql id = "xxx"> 标签,通过 <include refid="xxx"/> 引用。

开发社区首页(discuss_post 表)

功能拆分:开发社区首页,显示前 10 个帖子。开发分页组件,分页显示所有帖子。

用到的表是 discuss_post 数据库表,包括帖子 id、发帖人 id、标题、内容、类型、状态、发帖时间、评论数量(为了提高效率,避免关联查询,因此冗余存储)、分数(用于进行热度排名)。

开发数据层

帖子相关操作:

  • 创建对应 discuss_post 表的 DisscussPost 实体类。
  • 创建 DisscussPostMapper 接口,使用 @Mapper 注解。
    • 分页查询中用户 id 是可选参数,通过动态 SQL 选择,如果为 0 就不使用,在开发用户个人主页查询用户发帖记录时需要使用。
    • 如果只有一个参数,并且在动态 SQL 的 <if> 里使用,必须使用 @Param 加别名。
  • 创建 disscusspost-mapper.xml
    • where status != 2 拉黑的帖子不展现。
    • <if test="userId!=0"> userID 为 0 时不使用,按照类型,发帖时间排序

开发业务层

创建 DiscussPostService 类,可以分页查询帖子和帖子数量。

创建 UserService 类,实现根据 id 查询用户功能,因为显示帖子时不显示用户 id,而是显示用户名。


开发视图层

把静态资源 css、html、img、js 放到 static 目录下。

把模板 mail、site、index.html 放到 template 目录下。

创建 HomeController,getIndexPage 方法,用 map 集合把帖子和用户封装到一起。

修改 index.html,使用 <th:text="${map.xxx.xxx}" 动态替换。

【问题】使用帖子关联查询用户时,给查询用户的 findUserById 方法传入了帖子的 getId 方法,应该是 getUserId 方法。


开发分页组件

创建 Page 实体类,封装分页信息,包括当前页码、显示限制、帖子总数、查询路径等。显示的起始页不能小于 1,最大页不能超过 total。

index.html 中,当 page.rows > 0 时显示分页信息。

如果 page.current 等于 1 或 page.total,代表是首页或末页,此时不能点击上一页和下一页,用 disabled 属性实现。


开发注册登录模块


发送邮件

新浪邮箱打开 SMTP 服务。

引入 spring-boot-starter-mail 依赖。

在配置文件配置主机、端口、发送邮箱、授权码等。

创建 MailClient 类,调用 JavaMailSender 发送邮件。

使用 thymeleaf 发送 HTML 邮件,调用 TemplateEngine 把信息封装到 HTML 模板。

【问题】发送邮件成功但没接收到,在垃圾箱中可找到。


注册功能

把 register.html 地址关联到首页的注册 href 属性。

设置域名、创建 CommunityUtil 工具类,在工具类创建生产随机字符串和 MD5 加密方法。

创建 LoginController,创建 getRegisterPage 方法,跳转注册页面。

在 UserService 中创建 register 方法,判断注册信息合规后插入数据库,发送激活邮件。

在 LoginController 创建 register 方法,调用 UserService 的 register 方法。

创建接口 CommunityConstant,定义激活码的三种状态,成功、重复、失败,让 UserService 和 LoginController 实现该接口。

点击激活邮件的 url 【本地服务器的url】后,服务器通过 LoginController 的 activation 方法查询数据库用户,如果 url 中的激活码和设置的一样,就把用户 status 改为 1。


生成验证码

pom.xml 导入 kaptcha 的 jar 包。

创建配置类 KaptchaConfig,设置验证码的大小、范围、长度等。

在 LoginController 类新增 getKaptcha 方法生成验证码图片。

login.html 中,将刷新验证码的链接绑定 refresh_kaptcha 方法,通过 id 选择器获取 img 组件,重新访问 getKaptcha 方法生成验证码图片。

【问题】由于访问同一个生成验证码路径,需要在 url 参数加上一个随机数字,保证会重新请求获取新图片。


登录退出功能(login_ticket 表)

登录成功时,需要生成一个登录凭证发送给客户端。凭证可以在多个业务中连续地验证用户的登陆状态,凭证信息存储在 login_ticket 数据库表中,status 的 0 和 1 表示有效和无序,expire 表示过期时间。

创建对应 login_ticket 表的 LoginTicket 实体类,对应 login_ticket 数据库表。

创建 LoginTicketMapper 接口,通过 @Insert@Select@Update 注解来插入、查询、更新凭证。

在 UserServce

  • 创建 login 方法,验证账户合规后将凭证信息插入数据库,添加登录凭证到 map 中。
  • 创建 logout 方法,将对应凭证设为无效。

在 LoginController

  • 创建 login 方法,判断验证码正确后调用 UserServce 的 login 方法,如果 map 包含 ticket 代表登录成功,重定向跳转首页,否则添加错误信息并跳回登录页。
  • 创建 logout 方法,判断验证码正确后调用 UserServce 的logout 方法,跳转至登录页。

login.html 绑定登录链接,index.html 绑定退出登录链接。

【问题】登录成功后,创建了凭证,但忘记将凭证信息插入数据库。


显示登录信息

创建 CookieUtil 工具类,通过 name 查询对应 cookie 的 value。

在 UserService 中新增 findLoginTicket 方法,根据 ticket 查询 LoginTicket。

创建 HostHolder 类用来模拟 session 的功能,利用 ThreadLocal 实现,存储用户信息。

创建 LoginTicketInterceptor 拦截器,实现 HandlerInterceptor 接口。

  • preHandle 方法中通过 CookieUtil 的 getValue 方法查询是否有凭证 cookie,如果有则通过 UserService 的 findloginTicket 方法查询用户 ID,再通过用户 ID 查询用户。最后将用户放入 hostHolder 中。
  • postHandle 方法中通过 hostHolder 的 get 方法获取用户,并将其存入视图中。
  • afterCompletion 方法中清除 hostHolder 中存放的用户信息。

创建 WebMvcConfig 配置类,实现 WebMvcConfigurer接口,配置 LoginTicketInterceptor,拦截除了静态资源之外的所有路径。


上传头像

在 UserService 新增 updateHeader 方法,更改指定用户的头像。

创建 UserController

  • 新增 getSettingPage 方法访问账户设置 setting.html ,并在 index.html 的账号设置按钮关联该链接。

  • 新增 uploadHeader 方法更新用户头像,如果上传出现错误将错误信息存在 Model 对象中。

    如果没有错误,生成一个文件对象 dest,利用 MultipartFile 接口的 transferTo 方法将用户上传文件导入 dest,并从 hostHolder 中取出用户,更新用户的头像路径。

  • 新增 getHeader 方法获取用户头像,利用文件输入流读取图片数据,利用 HttpServletResponse 的字节输出流再进行输出。

调整 setting.html 的 form 表单, method=“post”,enctype=“multipart/form-data”,并设置提交路径。


修改密码

在 UserService 中新增 changePassword 方法,判断原密码是否正确,正确则修改密码并返回 1,否则返回 0。

在 UserController 中新增 changePassword 方法,根据 UserService 的 changePassword 方法的返回值判断原密码是否成功修改,封装为 JSON 数据并返回。

setting.html

  • 首先在前端判断两次输入的新密码是否一致,如果不一致不允许点击提交并显示错误信息。
  • 利用 ajax 向 UserController 的 changePassword 方法发送 POST 请求,得到 JSON 数据并解析,如果状态码为 0 提示错误,如果状态码为 1 弹出修改成功提示。

【问题】js 的虚拟路径问题,需要加上 ../

【问题】使用 ajax 请求时,表单按钮类型必须是 button,不能是 submit,否则 405 报错。

【问题】使用 ajax 请求时,Controller 中方法的返回值必须是 JSON 数据,并且需要加上 @ResponseBody

【问题】使用 ajax 请求时,回调函数需要先对返回的 JSON 数据进行解析再使用。


检查登录状态

利用拦截器,实现只处理带有自定义注解的方法,防止用户在未登录情况下通过 url 访问没有权限的页面。

创建 @LoginRequired 自定义注解,作用范围在方法上,有效期为运行时。

在 UserController 中需要在登录状态下调用的方法,访问设置页面、修改密码、上传头像等加上自定义注解。

创建 LoginRequiredInterceptor 拦截器,在 preHandle 方法中判断方法是否加了 @LoginRequired 注解,如果加了注解并且此时从 hostHolder 中获取不到用户则拒绝访问。

在 WebMvcConfig 配置类配置 LoginRequiredInterceptor,拦截除了静态资源之外的所有路径。


开发核心功能

敏感词过滤

利用字典树数据结构解决。
三个指针,一个指针首先指向前缀树根节点;另外两个指针指向待过滤信息的头部。交替移动对比,不匹配时候放入结果集,匹配的时候替换为指定符号。

创建 SensitiveFilter 类

  • 创建静态内部类 TrieNode ,通过 boolean 类型的结束符判断是否匹配到关键字尾部。
  • 利用 @PostConstruct 注解,在构造方法执行后初始化字典树。
  • 添加 filter 方法,利用双指针进行匹配,过滤敏感词。

【问题】判断子节点空时,直接添加了一个 new 的子节点,没有将对象赋值给子节点变量。


发布帖子

引入 fastjson 依赖,在 CommunityUtil 中新增 getJSONString 方法封装 JSON 信息。

在 DisscussPostMapper 接口新增 insertDiscussPost 方法,并在 disscusspost-mapper.xml 配置 insert 语句。

在 DiscussPostService 新增 addDiscussPost 方法调用 DisscussPostMapper 的 insertDiscussPost 方法,其中需要进行对标题内容和发帖内容进行 HTML 转义以及过滤敏感词。

创建 DiscussPostController 类,新增 addDiscussPost 方法,调用 DiscussPostService 的 addDiscussPost 方法发帖。

index.html 中为发帖按钮绑定函数,利用 Ajax 向 DiscussPostController 的 addDiscussPost 方法发送 POST 请求。


显示帖子内容

在 DisscussPostMapper 接口新增 selectDiscussPostById 方法,在 disscusspost-mapper.xml 配置 select 语句。

在 DiscussPostService 新增 findDiscussPostById 方法调用 DisscussPostMapper 的 selectDiscussPostById 方法。

在 DiscussPostController 新增 getDiscussPost 方法,调用 DiscussPostService 的 findDiscussPostById 方法查询帖子内容,将 DiscussPost 对象和 User 对象(通过 userId 查询,不在 DAO 层关联查询)数据存放到 Model 对象,返回模板 discuss-detail

discuss-detail.html 取出 Model 对象存放的数据绑定到对应组件显示。


显示评论(comment 表)

创建 comment 表对应的实体类 Comment。

创建 CommentMapper 接口

  • 新增 selectCommentsByEntity 方法,根据实体查询一页的评论数据。
  • 新增 selectCountByEntity 方法,根据实体查询评论的数量。
  • comment-mapper.xml 配置 select 语句。

创建 CommentService 类

  • 新增 findCommentByEntity 方法,调用 CommentMapper 的 selectCommentByEntity 方法。
  • 新增 findCommentCount 方法,调用 CommentMapper 的 selectCountByEntity 方法。

在 DiscussPostController 的 getDiscussPost 方法中增加查询帖子评论和回复的逻辑,将结果存储在 Model 对象。

【问题】sql 的 xml 文件中绑定参数时,应传入实体类属性名,拼错成数据库字段名(entityId 写成 entity_id)。


添加评论

在 CommentMapper 接口新增 insertComment 方法,添加评论数据,在 comment-mapper 配置对应 sql。

在 DiscussPostMapper 接口新增 updateCommentCount 方法,增加评论数量,在 discusspost-mapper 配置对应 sql。

在 DiscussPostService 类新增 updateCommentCount 方法,调用 DiscussPostMapper 的 updateCommentCount 方法。

在 CommentService 类新增 addComment 方法,调用 CommentMapper 的 insertComment 新增评论,并调用 DiscussPostService 的 updateCommentCount 更新评论数量,使用 @Transactional 注解保证事务。

创建 CommentController 类,新增 addComment 方法,从 hostHolder 获取用户信息,然后调用 CommentService 的 addComment 方法添加评论。

【问题】sql 的 xml 文件中绑定参数时,应传入实体类属性名,拼错成数据库字段名(entityId 写成 entity_id)。


显示私信列表 (message 表)

创建对应 message 表的实体类 Message。

创建 MessageMapper 接口,增加查询会话列表、会话数量、私信列表、私信数量、未读私信数量等方法,在 message-mapper.xml 中配置对应的 sql。

创建 MessageService,调用 MessageMapper 中的方法。

创建 MessgaeController

  • 新增 getLetterList 方法,将会话列表信息存储到 Model 对象,返回 letter 视图。
  • 新增 getLetterDetail 方法,将每个会话具体的私信信息存储到 Model 对象,返回 letter-datail 视图。

发送私信

在 MessageMapper

  • 新增 insertMessage 方法插入私信记录,在 message-mapper.xml 配置 insert 语句。
  • 新增 updateMessgae 方法修改私信状态,在 message-mapper.xml 配置 update 语句,利用 foreach 动态 sql。

在 MessageService

  • 新增 addMessage 发送私信方法,过滤敏感词后,调用 MessageMapper 的 insertMessage
  • 新增 readMessage 方法读取信息,调用MessageMapper 的 updateMessgae 更新私信的状态为 1。

在 MessageController

  • 新增 getLetterIds 方法,将私信集合中未读私信的 id 添加到 List 集合并返回,在 getLetterDetail 方法调用该方法设置已读。
  • 新增 sendLetter 发送私信方法,设置私信信息后调用 MessageService 的 addMessage 发送。

统一异常处理

在 HomeController 中增加 getErrorPage 方法,返回错误页面。

创建 ExceptionAdvice 类

  • 加上 @ControllerAdvice 注解,表示该类是 Controller 的全局配置类。
  • 创建 handleException 方法,加上 @ExceptionHandler 注解,该方法在 Controller 出现异常后调用,处理捕获异常。如果是异步请求返回一个 JSON 数据,否则重定向至 HomeController 的 getErrorPage 方法。

统一日志处理

pom.xml 引入 aspectj 的依赖。

创建 ServiceLogAspect 类,添加 @Aspect 切面注解,配置切入点表达式,拦截所有 service 包下的方法,利用 @Before 记录日志。


Redis

点赞

创建 RedisKeyUtil 工具类

  • 定义分隔符 : 以及实体获得赞的 key 前缀常量 like:entity
  • 新增 getEntityLikeKey(int entityType,int entityId) 方法,通过实体类型和实体 id 生成对应实体获得赞的 key。

创建业务层的 LikeService 类

  • 注入 RedisTemplate 实例。
  • 新增 like 点赞方法,首先通过 RedisKeyUtil 工具类的 getEntityLikeKey 方法获得实体点赞的 key,然后通过 RedisTemplate 对象对 set 集合的 isMember 方法查询 userId 是否存在于对应 key 的 set 集合中,如果存在则移除出点赞的用户集合,如果不存在则添加到点赞的用户集合。
  • 新增 findEntityLikeCount 方法查询实体的点赞数量,通过调用 set 集合的 size 方法查询元素个数。
  • 新增 findEntityLikeStatus 方法查询某用户对某实体的点赞状态,逻辑如 like 方法,通过 set 集合的 isMember 方法实现。

创建表现层的 LikeController 类

  • 注入 LikeService 和 HostHolder 实例。
  • 新增 like 点赞方法,调用业务层的 like 方法进行点赞、调用 findEntityLikeCountfindEntityLikeStatus 查询点赞数量和点赞状态,封装到 map 集合,然后通过工具类封装成 JSON 数据返回。

(更新首页帖子点赞数量)在表现层的 HomeController 类

  • 注入 LikeService 实例。
  • getIndexPage 方法在通过 LikeService 类的方法获得点赞数量,存储到 map 集合。

收到的赞

对点赞功能进行重构

在 RedisUnitl 工具类

  • 新增用户获得赞 key 的前缀常量 like:user
  • 新增 getUserLikeKey(int userId) 方法,通过用户 id 生成对应用户获得赞的 key。

在 LikeService 中

  • 重构 like 方法,在参数列表中加入 entityUserId 表示被点赞用户的 id,用来更新用户的被点赞数量。
    • 通过 RedisTemplate 对象的 execute 方法实现事务,保证被点赞用户点和点赞用户的数据更新一致。通过 isMember 方法查询用户的点赞状态,之后通过 mutli 方法开启事务。
    • 当用户已点赞时,调用 remove 方法将当前用户从点赞用户的集合中移除,调用 decrement 方法将被点赞用户的被点赞数减 1;当用户未点赞时,调用 add 方法将当前用户添加到点赞用户的集合,调用 increment 方法将被点赞用户的被点赞数加 1。
  • 增加 findUserLikeCount 方法,以用户 id 作为 key,调用 get 方法查询用户所获得的点赞数。

在 LikeController 中给 like 方法增加 entityUserId 参数即可。


关注

在 RedisUnitl 工具类

  • 新增用户关注实体(帖子、评论、用户等)和粉丝(用户)的前缀常量 followeefollower
  • 新增 getFolloweeKey(int userId, int entityType) 方法,通过用户 id 和实体类型生成用户关注实体的 key。
  • 新增 getFollowerKey(int entityType, int entityId) 方法,通过实体类型和实体 id 生成实体用户粉丝的 key。

创建业务层的 FollowService 类

  • 新增

    follow
    
    • 1

    方法,当用户关注某实体时,

    • 调用 add 方法将当前实体 id 和时间作为 value 和 score加入用户的关注集合。
    • 调用 add 方法将当前用户 id 和时间作为 value 和 score 加入实体的粉丝集合。
  • 新增

    unfollow
    
    • 1

    方法,当用户取消关注某实体时,

    • 调用 remove 方法将当前实体从用户的关注集合移除。
    • 调用 remove 方法将用户从实体的粉丝集合移除。

    在这里插入图片描述


个人主页

在业务层的 FollowService 类

  • 新增 findFolloweeCount 方法,调用 zset 的 zcard 方法查询某用户关注的实体数量。
  • 新增 findFollowerCount 方法,调用 zset 的 zcard 方法查询某实体的粉丝数量。
  • 新增 hasFollowed 方法,根据 zset 的 zscore 方法返回值查询当前用户是否关注某实体。

在 UserController 中新增 getProfilePage 方法获取个人主页。

  • 调用 LikeService 的 findUserLikeCount 查询用户获赞数,并添加到 Model 中。
  • 调用 FollowService 的findFolloweeCountfindFollowerCounthasFollowed 方法分别查询关注数量、粉丝数量、用户是否关注三项信息并添加到 Model 对象中存储。

关注列表和粉丝列表

在业务层的 FollowService 类

  • 新增 findFollowees 方法,查询用户关注列表,主要通过 zset 的 reverseRange 获取 value 即关注用户的 userId,再查询出其 user,之后通过 score 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回。
  • 新增 findFollowers 方法,查询用户粉丝列表,主要通过 zset 的 reverseRange 获取 value 即粉丝的 userId,再查询出其 user,之后通过 score 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回。

在表现层的 FollowController 类

  • 新增 getFollowees 方法,获取关注列表,存入 Model 对象。
  • 新增 getFollowers 方法,获取粉丝列表,存入 Model 对象。

优化登录模块

存储验证码

在 RedisUntil 工具类

  • 新增验证码前缀常量 kaptcha
  • 新增 getKaptchaKey 方法,通过一个用户凭证(由于未登录,利用 cookie 实现)获得对应验证码的 key 值(利用 string 存储验证码)。

在表现层的 LoginController 类

  • 重构 getKaptcha 方法,将验证码存入 redis,key 值是当前随机生成的一个字符串,同时将该字符串存入 cookie。
  • 重构 login 方法,从 cookie 中获得随机字符串,生成验证码的 key 值,然后获取对应的 value 值即验证码。

存储登录凭证

在 RedisUntil 工具类

  • 新增登录凭证前缀常量 ticket
  • 新增 getTicketKey 方法,通过字符串获得登录凭证的对应 key 值(利用 string 存储)。

在业务层的 UserService 类

  • 重构 login 方法,将登录凭证存入 redis 中。
  • 重构 logout 方法,先从 redis 中获取登录凭证对象,将状态设为无效再重新存储进 redis
  • 重构 findLoginTicket 方法,根据 ticket 字符串获得对应登录凭证的 key,然后从 redis 查询登录凭证。

缓存用户信息

在 RedisUntil 工具类

  • 新增用户前缀常量 user
  • 新增 getUserKey 方法,通过用户 id 获得用户的对应 key 值(利用 string 存储)。

在业务层的 UserService 类

  • 新增 getCache,从缓存获取用户信息。
  • 新增 initCache,从 MySQL 查询用户信息并存入 redis
  • 新增 clearCache,用户信息变更(更新头像,激活)时清除缓存。
  • 重构 findUserById 方法,首先调用 getCache从缓存获取用户信息,如果获取为 null 则调用 initCache

kafka发布系统通知

在项目中,会有一些不需要实时执行但是是非常频繁的操作或者任务,为了提升网站的性能,可以使用异步消息的形式进行发送,再次消息队列服务器kafka来实现。
在这里插入图片描述
在这里插入图片描述

ElasticSearch搜索引擎

项目中的搜索服务采用的是ES来实现。ES分片
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/572933
推荐阅读
相关标签
  

闽ICP备14008679号