牛客网项目总结
目录
数据库表用户表 user评论表 comment帖子表 discuss_post用户登录凭证表 login_ticket消息表 message
开发社区首页搭建基本环境开发社区首页(discuss_post 表)开发数据层开发业务层开发视图层开发分页组件
开发注册登录模块发送邮件注册功能生成验证码登录退出功能(login_ticket 表)显示登录信息上传头像修改密码检查登录状态
开发核心功能敏感词过滤发布帖子显示帖子内容显示评论(comment 表)添加评论显示私信列表 (message 表)发送私信统一异常处理统一日志处理
Redis点赞收到的赞关注个人主页关注列表和粉丝列表优化登录模块
kafka发布系统通知ElasticSearch搜索引擎
本项目是个人独立开发的后端项目,这要涉及到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评论目标 idcontenttext评论内容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登录用户 idticketvarchar登录凭证,随机字符串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 语句可以写在
开发社区首页(discuss_post 表)
功能拆分:开发社区首页,显示前 10 个帖子。开发分页组件,分页显示所有帖子。
用到的表是 discuss_post 数据库表,包括帖子 id、发帖人 id、标题、内容、类型、状态、发帖时间、评论数量(为了提高效率,避免关联查询,因此冗余存储)、分数(用于进行热度排名)。
开发数据层
帖子相关操作:
创建对应 discuss_post 表的 DisscussPost 实体类。创建 DisscussPostMapper 接口,使用 @Mapper 注解。
分页查询中用户 id 是可选参数,通过动态 SQL 选择,如果为 0 就不使用,在开发用户个人主页查询用户发帖记录时需要使用。如果只有一个参数,并且在动态 SQL 的
where status != 2 拉黑的帖子不展现。
开发业务层
创建 DiscussPostService 类,可以分页查询帖子和帖子数量。
创建 UserService 类,实现根据 id 查询用户功能,因为显示帖子时不显示用户 id,而是显示用户名。
开发视图层
把静态资源 css、html、img、js 放到 static 目录下。
把模板 mail、site、index.html 放到 template 目录下。
创建 HomeController,getIndexPage 方法,用 map 集合把帖子和用户封装到一起。
修改 index.html,使用 【问题】使用帖子关联查询用户时,给查询用户的 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 方法进行点赞、调用 findEntityLikeCount 和 findEntityLikeStatus 查询点赞数量和点赞状态,封装到 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 工具类 新增用户关注实体(帖子、评论、用户等)和粉丝(用户)的前缀常量 followee 和 follower新增 getFolloweeKey(int userId, int entityType) 方法,通过用户 id 和实体类型生成用户关注实体的 key。新增 getFollowerKey(int entityType, int entityId) 方法,通过实体类型和实体 id 生成实体用户粉丝的 key。 创建业务层的 FollowService 类 新增 follow 方法,当用户关注某实体时, 调用 add 方法将当前实体 id 和时间作为 value 和 score加入用户的关注集合。调用 add 方法将当前用户 id 和时间作为 value 和 score 加入实体的粉丝集合。 新增 unfollow 方法,当用户取消关注某实体时, 调用 remove 方法将当前实体从用户的关注集合移除。调用 remove 方法将用户从实体的粉丝集合移除。 个人主页 在业务层的 FollowService 类 新增 findFolloweeCount 方法,调用 zset 的 zcard 方法查询某用户关注的实体数量。新增 findFollowerCount 方法,调用 zset 的 zcard 方法查询某实体的粉丝数量。新增 hasFollowed 方法,根据 zset 的 zscore 方法返回值查询当前用户是否关注某实体。 在 UserController 中新增 getProfilePage 方法获取个人主页。 调用 LikeService 的 findUserLikeCount 查询用户获赞数,并添加到 Model 中。调用 FollowService 的findFolloweeCount、findFollowerCount 、hasFollowed 方法分别查询关注数量、粉丝数量、用户是否关注三项信息并添加到 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分片