仿牛客网项目记录
仿牛客网项目笔记,点击进入学习地址。
牛客网项目笔记
略有些乱,有空整理下。
启动项目之前需要启动的工具有:
1.打开MySQL workbench,有时候遇到数据库问题打开就没事啦
2.启动kafka(windows下经常奔溃,删除log试试)
要保证你当前的路径是D:\kafka_2.13-3.2.0下哦,用cmd启动
bin\windows\zookeeper-server-start.bat config\zookeeper.properties
bin\windows\kafka-server-start.bat config\server.properties
3.启动ES
bin文件夹下的elasticsearch.bat
Spring Boot进阶,开发社区核心功能
首页
在没有登录之前,首页会显示若干条网友的帖子信息,首先当然是查询我们的数据库一共有多少条帖子,需要注意有些帖子是黑名单,然后进行一个排序,因为有的帖子是加精华或者置顶。展示帖子的时候,需要考虑页数,因为你不可能展示所有页,可以保留本页的前两页和后两页,当然如果是第一页或者末页你需要考虑一些限制,除此外,你每页展示的帖子也有数量限制,具体可以看数据库怎么计算的。这里我们需要额外考虑一个功能就是查询我的帖子,这是后面额外的功能,可以用动态sql语句来拼接,如果登录了就可以传我的id来查询。默认情况下没有登录,后台首先查询到所有帖子的信息,根据帖子信息可以查询到userid然后可以查到作者名字(post数据库没有存用户名字),用一个map来存储两个参数,一个是post的内容,比如标题呀创建时间之类的,还有一个是username,再用一个ArrayList(名字为discussPosts)来保存这些map,然后通过model来注入传输到index。而计算总页数则单独用实例page来控制,包括页的范围之类。
HomeController用来返回主页,调取mapper来获得主页的页数以及内容方法
DiscussPostMapper定义两个查询数据库的方法
discusspost-mapper.xml具体的数据库语句
涉及两个实体Page和DiscussPost,分别是页的各种属性和帖子的各种属性
涉及网页Index.html
本方法没有用到业务层
关于前台传数据到后台,controller都是用String,对于首页在方法用写了Page page
需要注意的是,如果没有登录用户,不能显示我要发布这个功能
注册功能
输入账号,密码,确认密码,邮箱后,后台首先检查两次密码是否一致,然后检查账号是否存在,不存在的话发送一个链接到指定邮箱,只有用户点击邮箱后的链接才算激活成功。
User实体定义用户id,用户名,密码,盐,邮箱,类型(管理员之类,普通用户),激活状态,头像图片链接,创建时间
LoginController前台接收了信息后交给Userservice的register方法用于判断注册的有效性,比如邮箱是否已经被注册,然后无误后进行注册,有问题会返回一个map给ConTroller并显示错误信息,没问题则告知注册成功并发送首页然后跳转到首页。
register方法:判断完有效后,注册的时候首先生成随机盐,然后用随机盐+密码的合体转成md5写入表中,类型为普通用户,状态未激活,然后生成一个随机字符串用于生成激活码,接着生成图片和创建时间,写入数据库,接着把生成一个链接,含有激活码和系统为用户生成的id,发送到邮箱等待激活。激活成功会把激活状态更新为1,然后可以登录。
邮件是用模板引擎渲染的
UserMapper定义数据库方法,查id,email,name。更新激活状态,图片url,密码。创建用户
user-mapper.xml则是具体的sql语句
涉及网页register.html
工具类CommunityUtil用来生成随机数,这个不需要注入,提供的是静态方法
关于前台传数据到后台,前台定义了username,password,但是后台是定义User user,需要注意的是表单传入的数据可能会很多,通过实体接收比较方便,不然参数太多了。这也是需要对应的,表单参数名与实体属性名对应。
激活功能
登录功能验证码
登录界面会有验证码,先引入依赖kaptcha,然后再config中配置KaptchaConfig由/kaptcha响应图片,LoginController中的方法是getKaptcha,生成完验证码后存入session,并且将图片响应给浏览器,需要用到response。
登录功能
需要输入账号,密码,验证码。LoginController首先获取验证码,利用session来获取,首先来判断验证码是否正确,其他都不判断先,还要注意我们验证码本身是否没有响应。验证码正确后接着检查是否选择记住我,注意这是一个布尔值。将前台输入的信息交给Userservice的login来判断,判断成功后生成一个登录凭证(每次登录都是一个随机的凭证),这个凭证和id相关联,这样会安全一点,不需要暴露账号密码,用session来保存这个凭证,并由response响应给浏览器。
关于前台传数据到后台,前台定义了username,password,code,接收也是有这三个值,和注册不太一样,注册直接用实例,我觉得可能这里因为多了一个验证码的原因,user中没有验证码,所以直接定义来获取
登出功能
LoginCtroller首先获取String ticket,用@CookieValue来获取,然后调用userService.logout来注销,把登录状态改为1(也就是失效),然后重定向到登录页
拦截器
上面的登出功能单纯把数据库表的ticket变成失效,而拦截器是用来检查登录状态的,比如你登录了就显示你的信息。首先是定义HostHolder。里面有用户执行的方法,比如设置用户,移除用户。接下来写LoginTicketInterceptor,拦截前pre要去数据库看看ticket是否有效,然后到到了post,把信息放到modelAndView的loginUser属性里,这样可以在前端判断是否正确登录了然后并显示,登出之后会销毁。这里和logout的联系关系是,logout设置数据库状态为1,然后pre的时候就可以判断了,如果注销了就执行after把hostHolder销毁掉。
忘记密码并重置
前台输入邮箱,然后点击获取验证码,这里需要注意用到了javascript来获取验证码,跳到了路径/forget/code,然后Controller的getForgetCode开始验证邮箱是否存在(Userservice.verifyEmail),若存在就发送邮件并且把 验证码传到session中保存,输入验证码提交跳转到/forget/password。由controller的resetPassword进行校验和修改密码Userservice的resetPassword别忘了更新密码的时候要获取盐再md5保存。可以学习下setting.json写法
更新密码
这个情况是已经登录了在设置中进行更新密码。所以首先在Controller中用hostHolder获取用户信息,然后由userService.updatePassword来校验原始密码并修改,注意了,新密码和确认密码前台来判断即可。老样子,更新密码别忘了盐。
Spring Boot进阶,开发社区核心功能
过滤器
有了这个功能可以自定义过滤词来替换一些不好言论,需要注意的是要考虑☆赌☆博☆这种情况,因为大家一般不会直接写赌博。首先是定义前缀树的数据结构,然后把我们的过滤敏感词加载到前缀树中初始化。
1号指针指向前缀树,2号指针和3号指针指向我们的文本,如图所示,敏感词有abc,bf,be,我们的文本是xwabfacff,还要注意的是前缀树需要在叶子节点处标记末端,用false标记
这里大概讲讲是怎么个过程,详细的具体回顾请debug。第一步生成一个StringBuilder来保存没有敏感文字,然后1号指针指向root,注意root不存储任何的敏感文字,2号指针(begin)是大循环,先判断begin是否超过文本长度,然后进入循环,判断3号指针是否超过文本长度。情况1先判断是否有非法符号,比如☆,就跳过这些。然后把字符放进前缀树中查找子节点是否有,没有的话把这个字符加进StringBuilder,然后begin移动一位,同时position移到begin的位置,然后注意了,这时候指针1重新指向根节点,情况2继续while循环,如果下级结点和我们的字符匹配到了,然后移动position,然后begin先不动,情况3继续while循环,如果下级结点匹配到了叶子节点,也就是发现了敏感词,StringBuilder中存入,position移动一位,begin也移动到position的位置,情况4*还有一种情况是如果发现了符号,比如☆,如果这时候前缀树还指向根节点,那我们就把符号加进去StringBuilder,如果前缀树指向不在根节点,就直接移动position,跳出循环。最后一种情况5,也就是position越界了,但是还没匹配到敏感词,这时候先把begin指向的字符加进去StringBuilder,然后begin移动一位,接着position回到begin的位置,还有前缀树重新回到根节点。
情况5不好理解,可以试试敏感词为 fabcd,abc。text为ccfabc,debug后你可以可以知道怎么回事了。
涉及SensitiveFilter和sensitive-word.txt
发布帖子
Contoller用于生成对象DiscussPost来获取题目和内容。然后交给service来处理这些数据写到数据库中,需要注意的是,service部分需要判断用户输入的内容是否合法。前端是在index上用ajax来触发post的,具体看index.js,具体的逻辑是button上点击提交后,ajax会调用一个函数,然后函数传到指定的路径来请求。数据库插入在xml文件中,mapper中有数据库的操作函数
帖子详情
首页上出现帖子,点击后请求/discuss/detail/${map.post.id}|}”,具体在128行,controller在DiscussPostController,DiscussPostService有查找post_id的方法,然后显示在discuss-detail.html上
显示评论
先解释下comment这个数据表,entity_type表示实体类型,比如1代表帖子,2代表用户,3代表题,4代表用户课程。entity_id表示id为多少的这个帖子。target_id比如你可以对某个帖子评论,或者在评论中回复别人,有指向性。status表示这个评论是正常还是被删除了。
这个业务比较繁琐,首先我们上面说了实体类型有不同,而且还要考虑回复的回复。具体代码看DisCussPostController的帖子详情内部方法。这里面可以仔细看看代码部分,因为涉及的逻辑比较多。还要就是看discuss-detail.html是怎么读取内容的
添加评论
评论的话有三类,第一类是直接对帖子进行评论,第二类是评论中的普通评论,第三类是评论中的回复。
还需要注意的是,在评论完之后 要更新帖子评论的数量,这是一个事务,必须两个一起完成,具体看Commentservice.还需要看commentcontroller。涉及操作的网页是discus-detail。包含了三种回复帖子的不一样格式,比如实体类型不一样,如果是回复别人,还需要拿到target的id。
私信列表
数据表如图,fromid发送人,fromid为1是系统通知,toid收件人,status 0-未读;1-已读;2-删除。coversation_id比如111发给112就拼为111_112,注意112发给111也是111_112,永远都是小号在前,当然了,其实这个数据是冗余的,但是为了后面我们查询方便才加入这个字段
这个章节需要理解数据库的查表sql语句,有些不好理解,在message-mapper.xml中。
涉及的网页有letter.html和letter-detail.html(这个网页有个js,在下面的back())。还有仔细看MessageControl的代码,需要注意一个业务细节,就是我们查私信都是通过会话来查,会话id是两个人的id组合,然后显示来自xxx的私信,必须是对方的名字,controller中特地写了getLetterTarget这个方法,可以看看。
发送私信
业务逻辑就是点击消息,然后根据名字来发,用户点击消息代表已读然后设置状态为1,具体实现在MessageController,这里发送内容用到了异步请求,具体看letter.js。有一个sql语句第一次见,具体看message-mapper.xml,更新信息状态那个,因为是多个消息,用到foreach
统一处理异常
具体看control包下的advice包的ExceptionAdvice。需要注意有异步请求的错误,有普通页面的错误。网页一个问题:这节课一开始说把错误页面的模板放到error文件夹下,服务器出错了就会自动连接到那个页面打开,那为什么后来还要专门定义一个类然后出错了在方法里重定向到错误页面呢?回答服务器只是在错误发生时跳转到错误页面,但是我们还需要记录日志,并且在异步请求中不是返回页面,而是返回json,这些是服务器不会帮你做的事情。总的来说,这个功能不也是不需要每个功能里具体写异常。
统一日志管理
这里有个抽象的概念叫AOP,具体可以百度,实现方法在aspect中,可以做到对所有Service类进行记录,当然也可以其他类,这样就不需要在某个controller上具体写某个日志。拦截器是AOP思想的一种实现,Spring AOP也是一种实现。
删除私信
开发删除私信功能,即点击某条私信的删除按钮时,将其状态设置为删除态。在没做之前考虑的难点,界面上是有一个x的,那么怎么触发这个x的操作?没错 ,这个功能也是要用到异步请求的。这里很重要的一点是,需要在letter-detail.html中设计一个隐藏的数值来保存这个会话的id(127行),然后用js异步请求。js获取到id后交给controller处理,controller交给service把状态改成2,并返回0。需要注意,只要有一个人删除了,对方都看不见这个消息,这是设计表没有考虑的问题。
Redis,一站式高性能存储方案
redis配置
redis需要配置依赖,并且在application.properties中配置相关redis,重新设置序列化方式。然后还要一个点注意,需要重写bean,具体看config中的redisconfig。可以看看test中的各种测试来巩固。redis就不写dao了,直接在service上操作存储。
点赞
先写生成key的工具,看util代码
三个地方点赞,一个是帖子,另外是两种评论(一个回复,一个普通)点赞 。需要在HomeControl中处理首页的赞,同时在DiscussPostController处理3个地方的点赞,这里解释下js语句的这个: href=”javascript:;” th:onclick=”|like(this,1,${post.id});|” 这里写用空的javascript,如果识别到空,就触发点击事件的like函数,因为里面有静态值,所以加||,第一个this的作用是获取这个赞是点哪里的,因为我们说了一共有3个地方有,然后1代表是帖子的赞,然后2代表评论和回复的赞,获取完后用异步来刷新界面。这里具体代码看likeservice和likecontroller,因为不需要用数据库,不写mapper。最后提一个没有实现的功能,就是未登录状态下你是不能点赞的(虽然报错服务器异常),后期老师说用spring security来实现
我收到的赞
先写生成key的工具,看util代码
这个需求需要把上一个点赞功能重写一下,本功能重写在likeservice中,总体的思路就是用userid来标记获得的赞,然后最后查询redis中的赞数量即可。这里理清楚两个key,一个是followee(单词意思为被追随者,也就是偶像,可以理解为里面的值是我的偶像,用来记录我关注了谁),一个是follower(单词意思为追随者,也就是粉丝,用来记录我被谁关注,可以理解为里面存的值是我的粉丝),
比如xixi(151)关注了aaa(111)。那么 followee:151:3的值是111 ,follower:3:111的值是151,如果xixi再关注niuke(149),此时 followee:151:3的值是111,149。3代表是关注用户,因为1代表关注帖子,2代表关注评论。
具体涉及的代码有UserController,FollowService,profile.html中有一个修改样式的代码,在关注TA那里,当判断已经关注就改变一个样式,这里还用到了JS的异步请求,可以仔细看看。
关注列表
先写生成key的工具,看util代码
需要写的方法有查询关注实体的数量,查询实体的粉丝的数量,查询当前用户是否已经关注该实体,查询某个用户关注的人,查询某个用户的粉丝。均在followservice中写,还有followcontroller中。涉及的网页有followee.html,follower.html。
优化登录模块
验证码部分
先写生成key的工具,看util代码
对验证码存入redis中,并设置过期时间,首先需要浏览器给一个临时凭证,然后存入cookie中,然后redis凭证的保存的值就是验证码,有效期为60秒,比如 kaptcha:ba60a25644194e438007dac1129d45f2存的验证码值是N2IZ,这段代码在LoginController的getKaptcha操作,取消原来的session。然后在login中用cookie来提取信息,判断是否有效,去掉原来的session,并添加cookie取值。这里一开始不能理解为什么要一个随机凭证,因为我们通过随机凭证可以取到验证码的值,而随机凭证是用cookie来保存
ticket部分
先写生成key的工具,看util代码
在userservice中处理,同时废弃之前写的loginticketmapper。首先在userservice的login方法中存入loginTicket,注意这是一个对象。在我们进入每个业务的时候都会寻找这个ticket,所以在findloginticket中就是返回redis的ticket,那么退出的时候,我们是把状态设置为1,然后再重新存入这个ticket,,在处理过程中注意要转化。
在登录的时候,redis是这样的:
ticket:b7d3e1f966fc44ee975586c992fccc91,他的值是”{"@class":"com.nowcoder.community.entity.LoginTicket","id":0,"userId":151,"ticket":"b7d3e1f966fc44ee975586c992fccc91","status":0,"expired":["java.util.Date",1653314918710]}”然后如果你退出登录,那么status就为1
需要注意,这个信息是永久保存的,而不是像验证码有那样,因为我们可以用来做一些统计信息
缓存用户信息
先写生成key的工具,看util代码
因为每次调用ticket的时候都要去调用用户id,所以这个功能也是比较频繁的。
在UserService中需要写三个方法,1.优先从缓存中取值2.取不到值就初始化缓存3.当数据变更,清除缓存数据。
那么在哪里会用到呢?首先是findUserById,如果redis中有,那么就用,没有的话就初始化,初始化的时候会调用一次mysql,然后再也不需要频繁调用了,直接用redis。其次还用到更新,比如激活码如果激活成功,那么状态就会改变,这时候就删除原来的缓存,然后如果系统识别不到缓存,就又重新生成一个redis,还有一个更新操作,就是更换头像,会更新headerurl,如果更新了,那么也会清除原来的缓存然后重新生成。
这里举个例子,当我们访问首页的时候,会有帖子,这些帖子的信息有发帖子的资料,我们就可以从redis中获取,然后会生成以下资料
然后每个key的具体值就是mysql中的信息
{"@class":"com.nowcoder.community.entity.User","id":145,"username":"lhh","password":"d980a16ea0b3c8a81062ee806e65a4bc","salt":"5abfc","email":"nowcoder145@sina.com","type":0,"status":1,"activationCode":"f217b637e9544e2a9b4a88f78c583d03","headerUrl":"http://images.nowcoder.com/head/145t.png\",\"createTime\":[\"java.util.Date\",1556436636000]}“
我的回复
我的帖子和我的回复是差不多的,对于这两个操作是在usercontroller中完成视图操作。首先需要传回去userid,在路径中进行拼写,之前我们在redis中重写userService.findUserById,通过userid来获取账号的信息,再用findUserComments获取全部的回复信息,这里的sql语句是新写的,可以看看,另外还有一个评论数量的sql语句也是新写的。同时需要注意,因为我们是在帖子中回复,所以还要找到相应的帖子,可以链接过去,所以在保存信息的时候,需要存post的内容
我的帖子
我的帖子查询之前已经写了sql语句了。那么除了获取帖子,还需获取一个点赞的数量,同样在UserController中写。
Kafka,构建TB级异步消息系统
发送系统通知
首先设计好event的实体属性,这里可以好好看一个点,因为部分属性进行了,接下来设计好生产者和消费者(在event包下),本项目对帖子,评论,回复,点赞和关注会发送系统消息,需要在CommentController,FollowController以及LikeController进行添加通知的方法,为此有些数据库操作多了一些,可以看xml的数据库操作语句,对于controller中,我们要学习一个写法,就是set那一部分,new完event后马上set,第一次看到这种写法。
显示系统通知
首先要清楚,只会显示最新的那个通知,可以看sql具体怎么操作提取最新。通知一共有三类,一个是评论通知,一个是点赞通知,一个是关注通知,其中评论和点赞的详情页都会链接到帖子处,关注的详情可以链接到这个人的主页。除此之外,还需要统计未读数量,以及已读设置,这个已读设置是点击详情页,然后你当前页都会设置已读。涉及的网页有notice.html,notice-detail.html。在messagecontroller,messageservice,message-mapper.xml。
朋友私信和系统通知都可以提取未读数量,然后相加就是消息的未读数量。这里要用拦截器,拦截器为MessageInterceptor,写完拦截器后需要在config的webmvcconfig中配置,最后再index.html中读取消息的总数量。
Elasticsearch,分布式搜索引擎
安装
除了[安装elasticsearch](免费且开放的搜索:Elasticsearch、ELK 和 Kibana 的开发者 | Elastic),还需要安装中文的分词,需要注意ES的版本要匹配spring boot,同时中文的分词ik也要对应ES。需要把ik解压到ES的plugins的ik目录下(自己创建)。另外还需要安装postman(非必要软件)来模拟客户端。需要注意的是,老师用6.x的版本,而我现在用7.x的版本,是有很大差别的,具体可以看(SpringBoot整合ElasticSearch7.12实现增删改查及高亮分词查询_夜中听雪的博客-CSDN博客)。还有这个(ElasticSearch(2)-RestHighLevelClient - 快乐的海盗 - 博客园)。
还有一个地方需要注意,redis是基于netty,elsaticsecrch也是基于netty,然后如果一起运行的话会冲突,在main方法上做了一个配置。
本章节的安装涉及页面有Esconfig,DiscussPostRepository,当然也要在application.properties配置。
开发社区搜索功能
最开始的时候老师完善了一个bug,在discusspost-mapper.xml中的insertDiscussPost方法中了加了keyProperty=”id”,如果不声明这个,mybatis就不知道增加的时候哪个是主键,就不会把生成的主键增加到实体类去,所以后面要想用到这个实体类,就需要加上。
为什么评论也要异步更新到搜索?因为最终呈现搜索结果的界面每一条帖子的最后面都有对应的点赞和回复信息,所以得刷新es。
首先在comment和discusspost的controller设置触发事件,也就是当我们发布帖子,或者评论的时候,都需要触发事件,这个就要用到消息队列,然后消费者可以根据id来覆盖帖子或者新建帖子,这样搜索引擎就收录了这些信息。
具体代码看Elasticsearchservice,EventConsumer,searchcontroller,在service代码中搜索部分是配合下面的分页来操作的,这个和老师不一样,因为版本的问题,这个分页用到pagehelper包
项目进阶,构建安全高效的企业服务
Spring Security
退出登录要用post请求,可以变成表单
权限控制
之前做的登录拦截废弃掉,在webmvc中废弃,现在用spring security来控制。需要注意的是spring security自带csrf拦截,,提交表单的时候会有一个随机token,但是你得去配置才能有最好的效果,有点麻烦,比如index有个例子,本项目我们disable了csrf这个功能。我们只做了授权配置,也就是用户类别的判断,没有做认证,还是用原来的认证方案,如果想了解可以看7.1的视频。
首先在CommunityConstant中声明三种权限,然后在SecurityConfig中来说明哪些权限能登录哪些页面,这些是需要自己统计的页面,然后写没有登录或者没有权限的时候是如何返回信息,这个可以好好看哦,还有这里多设计了一个拒绝访问的页面,在HomeControl中加入,也就是你权限不足相当于没有这个网页。那么在UserService中则写一个根据用户来获取用户权限的方法,写完这些还需要在LoginTicketInterceptor中获取用户凭证后面来构建用户认证的结果。对于退出登录,需要在LoginController中的logout方法清理内容。
评论中老师的一个说法:
1. Security提供了认证和授权两个功能,我们在DEMO里也做了演示,而在项目中应用时,我们并没有使用它的 认证功能,而单独的使用了它的授权功能,所以需要对认证的环节做一下特殊的处理,以保证授权的正常进行;2. Security的所有功能,都是基于Filter实现的,而Filter的执行早于Interceptor和Controller,关于Security的拦截器原理,可以参考http://www.spring4all.com/article/458;3. 我们的解决方案是,在Interceptor中判断登录与否,然后人为的将认证结果添加到了SecurityContextHolder里。这里要注意,由于Interceptor执行晚于Filter,所以认证的进行依赖于前一次请求的拦截器处理。比如,我登录成功了,然后请求自行重定向到了首页。在访问首页时,认证Filter其实没起作用,因为这个请求不需要权限,然后执行了Interceptor,此时才将认证结果加入SecurityContextHolder,这时你再访问/letter/list,可以成功,因为在这次请求里,Filter根据刚才的认证结果,判断出来你有了权限;4. 退出时,需要将SecurityContextHolder里面的认证结果清理掉,这样下次请求时,Filter才能正确识别用户的权限;5. LoginTicketInterceptor中的afterCompletion中其实不用清理SecurityContextHolder,将这句话删掉。
置顶,加精,删除
增加两个sql语句,具体看discusspost.xml。其次要在SecurityConfig中声明权限,管理员可以删除帖子,版主可以置顶和加精,注意在删除贴子的时候,我们还需要把ES搜索引擎的帖子数据也要删除,所以在Event消费者中添加消费删除的主题。在DiscussPostController中写好三个方法。涉及的前端页面是discuss-detail.html。我自己还修改了下js文件,加上取消置顶和取消加精。还有前端需要配合thymeleaf的spring security来检验权限,让没有权限的人看不到相关按钮。
统计用户数据
一个是访问量(UV),一个是活跃用户(DAU),这两个用的数据类型是不一样的,首先访问量用的是HyperLogLog,可以大致统计,DAU用Bitmap,可以精确统计。先在RedisKeyUtil中写好DAU和UV的数据格式,接着在DataSercice中写好方法,之前我们说过,redis不需要用mapper,直接在service中写好存数据的方法,然后要做一个拦截器,具体看DateInterceptor,到WebMVC中配置,再写一个DataController设置好访问的路径,只有管理员可以访问/data,这个在security中配置。涉及的网页在site/admin/data。
需要理解下DAU用OR运算的原因。
关于Bitmap和HyperLogLog有测试类,可以去看看。
任务执行和调度
需要配置quartz和线程池的东西哦,在application.properites
比如我们想半个小时统计一次帖子热度,清理临时文件,就需要用到这个任务。JDK和Spring的线程池只能各自为战,在分布式中不好用,而Quartz可以用在分布式中比较好用。
本节课讲了好多配置,测试类可以看ThreadPoolTests(我们还在AlphaSercice中写了相关方法),QuartzTests,
配置类看ThreadPoolConfig(里面单纯配置了下,好像是测试的时候可以用到@Async),还有QuartzConfig
quartz包还有一个AlphaJob
热帖排行
思路就是每隔多少时间计算一次,注意不是每个帖子都计算,而是如果触发了某些行为,比如点赞,加精,评论,还有新发布的帖子,这些帖子才计算,先把这些帖子放到redis中,然后到了时间就计算。
首先要设计rediskey,在RedisKeyUtil中,接来下就是写任务job了,在PostScoreRefreshJob中,写完Job后要配置Quartz,具体看QuartzConfig。最后还需要修改下数据库discusspostmapper的selectDiscussPosts方法,具体看xml的sql语法,相应的service中也要改方法哦,最后则是改变下HomeController的传参,这是新的一个例子,我们默认如果orderMode是1就热度排行,否则0就是默认排序,所以在index上和原来要改变下,注意看page.setPath的路径,多了一个order,然后model中也要加入这个属性。那么在index.html中传参也注意下,这是第一次看到的。
生成图片
分享的时候要用到,需要用到消息队列哦,用的技术是属于wkhtmltoimage
首先配置相关信息,生成图片命令和保存图片路径。然后在WKconfig中写一个生成文件夹的工具,因为程序会首先运行config类,接着写share的消费者,因为我们是用队列来运行的,最后写sharecontroller,除了生成长图,我们还要展示长图,输入正确的url后会返回一个链接,这个链接打开就可以在浏览器中展示图片。
上传文件到七牛云
配置好相关七牛云的信息哦
上传人物图像是属于客户端上传,分享图片是服务器直传。
首先是人物图像,要重写之前在本地保存的路径,具体看UserController,写完之后,需要在setting中修改上传图片相关代码,大概在setting.html的90行,另外还有js文件也要看哦。
分享的图片保存到七牛云,同样也是通过消息队列,所以要对eventconsumer进行一个重写。另外在sharecontroller中的map部分修改了下,主要大改的代码还是消费者那里,上传图片的时候需要考虑图片生成完没有呀,因为我们知道图片生成是稍微慢一点的,还要防止图片一直上传占用服务器的资源,所以我们要用上传时间和次数来限制这个失败的情况。
优化网站性能
和用户相关联的最好不要用本地缓存,比如登录凭证,因为我们是分布式部署,可能下次就不是请求这个服务器了。对于redis,服务器1第一次请求DB然后存到redis,服务2请求的话可以直接访问redis,就不需要用DB(database数据库)。本地缓存是比redis快的,而redis适用性广。
数据变化的频率相对较低适用缓存,默认的首页不适用哦,因为经常有人发帖子,而热门帖子适用。
不建议用spring来整合那几个常用缓存工具,因为他是统一管理,而每个工具都有自己独特的性能,如果单独设置又比较麻烦。
我们用的是Caffeine
先在配置上定义一些常量,然后主要是优化热门帖子和帖子行数,具体看DiscusspostService。
可以用jmeter来测试,具体看吞吐量来对比。
删除分享图片的本地资源
因为我们把图片是先保存到本地,然后再传给七牛云。所以在本地上的图片传完后可以删除,先写一个WKimageDeletejob,然后再quartz中写任务和触发器
项目发布与总结
单元测试
很有意思的是,以前我不写@RunWith(SpringRunner.class)也能运行,但是这次不行。代码在SpringBootTests,感觉跟之前的差不多呀。但是写了一些判断条件,然后自己可以运行下看看,跟以前的测试感觉差不多呀,不过多了一些判断。
项目监控
依赖spring的actuator,我们可以在配置类中配置相关类,官方手册是20+把。然后也可以自己自定义来写,比如看actuator包中的DatabaseEndpoint这个例子,当然了,这个需要管理员才可以看,我们还要在SecurityConfig中配置。
部署
centos,能用yum就这个命令下载,但是可能版本比较旧,可以从官方网站上下载链接更新。
数据库表及一些业务逻辑整理
数据库表
comment
1 | CREATE TABLE `comment` ( |
discuss_post
1 | CREATE TABLE `discuss_post` ( |
login_ticket,这张表到了后期是用不到了,因为ticket存在redis中,但是可以参考下redis中也是这样设置参数的
1 | CREATE TABLE `login_ticket` ( |
message
1 | CREATE TABLE `message` ( |
user
1 | CREATE TABLE `user` ( |
部分业务逻辑梳理
注册和登录
1.先说注册,需要的数据有:账号,密码,确认密码,邮箱
密码和确认密码由前端进行判断
后端要养成一个习惯,空值处理,然后判断账号和邮箱是否存在,如果都不存在就注册成功并发送邮件让用户激活
这里有个用户体验的地方,就是如果用户即便注册不成功,也要显示他之前写的数据在网页上,而不是刷新变成空
2.然后是激活,会发一个邮件给用户的邮箱,里面是含有有激活码的链接,点击即可激活,这样账号才能使用,注意要有用户重复激活和自己故意输入错误激活码的行为
3.最后是登录
登录的时候需要验证码,这个验证码用redis来存储,可以用若干时间后失效,如果验证码失效,需要重新更新redis
先判断三种情况1.验证码本身是否有问题,因为可能刷新不出来,这是我们的锅2.用户没有写验证码3.用户验证码是错误的
通过了上面判断,我们再检查账号密码,正确无误后,我们生成一个用户凭证,也就是ticket,每次登录都会生成,这是为了判断是哪个用户,同时这样也不会泄露用户的信息
同时为了避免每次都要看数据库来确认用户,我们把凭证存到redis中,对于用户的话,这个ticke是放在cookie中的
4.登出
把redis的loginticket状态设置为1,也就是无效。
这里提一下拦截器,拦截器是用来判断用户是否登录的,用户登录成功cookied就包含这个ticket,然后拦截器的pre先来判断是否有效,然后用hostHolder设置持有用户
然后查询他的用户权限,比如是否版主,交给spring security认证和授权。接着在执行模板前获取信息,返回loginUser到网页中,登出的话拦截器也会进行清理。
帖子,评论
在没有登录之前,也会有帖子的信息,有默认帖子和热门帖子,热门帖子是根据分来来进行排序的,默认帖子则是根据置顶,加精以及时间来排序的。
那么一个帖子会展示发帖人,帖子标题,点赞数量(redis中)以及回复的数量(数据表本身存有这个,不是放在redis中的),统一放在List<Map<String, Object>>中。
发布帖子,登录后才会显示我要发布帖子的按钮,这个显示就是拦截器的loginUser来判断的,发布帖子之前也要判断是否存在用户,虽然我们有拦截器
系统获取id,标题,内容,创建时间后,然后通过发帖服务来加到数据库中,同时触发发帖事件,这个的意义是为了把帖子的信息慢慢加入到搜索引擎ES中,所以才用kafka的消息队列,还有就是计算分数放在redis中
查看帖子,每个帖子的链接都有一个id来识别,需要查询的信息比较多,帖子的基本信息(标题,发帖人,点赞数),同时如果有用户登录,还需要判断是否点赞,接下来就是获取评论的信息了,包括评论,回复之类的,回复数量。
同时如果你是版主的话,是可以置顶和加精的,这两个操作也是用到消息队列,和发帖的topic是一样的,因为是为了更新ES的数据,ES是把整个帖子的数据库信息都录进去,所以要更新
评论,首先就是判断你的登录状态了,然后获取你的信息,评论更新到数据库后,同样触发消息队列的评论/回复,会通知相应的用户,显示到消息中,评论帖子操作是会触发更新分数的,同时也要放到消息队里中更新ES(因为帖子分数变了)
管理员是可以删帖子的,这个也会触发消息队列,因为要发ES中的内容删除
另外别忘了上面所以涉及文字内容,都需要过滤敏感词哦
私信,消息功能
发送私信,需要输入另外一个人的账号(独一无二的),写好内容发送后更新到数据库,这里没有用消息队列,只用了一个status来判断是否已读。
消息展示分为朋友私信和系统通知
私信一页会展示最多5个联系人,每个联系人上面会显示最新的一个对话,具体是用sql语句的select max(id) from message来获取,接下来就是获取时间以及数量之类的
点进联系人会是你们的详细聊天记录,url用一个固定conversationid来设置,然后通过这个id查询,这里还有一个设置已读的操作,具体实现就是,你每点击一页(消息也是分页的),就把这一页的都设置已读,因为你看到这一页的是经过查询才看到的,所以查询到东西就可以设置已读,每点击一页查询一页。
删除私信就是把状态设置为2的操作
还有个系统消息,分为评论,点赞,关注,然后分别点进去就是详细的内容,大概逻辑和私信差不多
评论点赞消息就不说了,这里讲下关注,关注有人,帖子和回复(这两个没实现,单纯用entitytype来提前记录),关注会用到消息队列来发送系统通知,
同时redis中用到一个事务,这个事务包括两个操作,更新我关注的人,同时更新这个被关注人的粉丝情况,取消关注也是用事务,一起更新。
当然,在个人信息(也可以看别人的)可以显示我关注的人和关注我的人,以及一共获得点赞数,这些都是存在redis中的数据
账号设置,更新密码,忘记密码
账号设置中只有两个操作,更换图像和设置密码
更换图像这里后面改成用七牛云才存储,这个也用到了消息队列,生成文件名是随机的,避免冲突
至于更新密码,则是输入旧密码,然后输入两次新密码即可,新密码判断是否一样在前台判断
忘记密码是在登录页面中的,具体逻辑是通过邮箱来修改,输入正确邮箱后会异步请求forget/code这个地址会生成一个验证码,然后发送一个随机验证码到邮箱上,同时session来存这个验证码用于后续判断用户是否输入正确。
其他杂七杂八
分享图片,也就是截图该页面保存到七牛云,这个只能手动输入url
统计日活,权限控制,threadlocal用在保存user信息中,保证并发时隔离线程(代替了session)
牛客网部署
mysql
注意这个要下载yum仓库,安装了好多次,最后大概用的代码如下
1 | yum clean all |
maven
不知道为什么在linux上一直构建不成功,索性直接在windows上构建,然后把war包放在tomcat目录下。
ES
和老师用的6.x版本不一样,直接用7.12版本。需要注意的时候,启动不能用root用户,然后如果你没有给普通用户权限的话,可能会启动失败。
wk
没有太重视这个功能,在上线的时候砍掉,安装的时候有点问题。
kafka
也没啥注意的,正常安装运行。
redis
没啥注意的,直接安装即可。
tomcat
这个比较坑,在window上用的是tomcat9,但是服务器一开始安装了一个10,死活启动不成功,最后换成9可以了。