1.开发社区首页
1,查询帖子列表这里参数需要多传一个用户id,因为后面需要点击进去帖子详情
2,这里使用动态sql作为拼接userId,不等于0的时候才拼接,status
等于2的时候是拉黑排除掉
3,安装类型时间降序排序
2.开发登录模块
2.1 邮箱
1,引入邮箱依赖
2,邮箱参数配置
3,使用javaMailSender
发送邮件,使用 ``MimeMessageHelper构建
MimeMessage`
4,参数发送人,发送的主题,发送的内容
5,使用模板发送,注入TemplateEngine(模板引擎)
,需要实例化Context设置发送的内容
2.2 注册
1,业务层需要判断controller传user是否为空
2,判断账号用户密码邮箱不能为空,为空发送错误页面
3,根据用户名查询数据库是否有数据,如果有直接给个提示
4,注册用户
4.1,设置盐加密0到5位,设置用户密码加密,设置类型为0普通用户,设置状态为0未激活,设置激活码,设置用户头像,用牛客网头像使用随机头像new Random().nextInt(1000)
,设置日期,最后添加到数据库
4.2,激活邮箱:
实例化Context,设置变量邮箱,url跳转地址,templateEngine.process(路径,context)返回内容字符串
,最后调用工具类发送邮箱
5,controller层先判断传的值是否为空值,和空字符串,如果不为空说明有异常
6,controller激活邮箱方法,需要获取路径中的参数(用户id,激活码)
7,需要在业务层写一个方法判断(激活,重复激活,激活失败)
7.1,根据用户id查询如果状态为1说明激活过了,否则传过来的验证码是否一致然后调用修改的方法把状态改为1,其他激活失败
2.3 会话管理
2.4 验证码
1,需要导入kaptcha
依赖
2,验证码controller
2.1,注入Producer
@Bean
2.2,调用createText()生成验证码
2.3,调用createImage()转换为图片
2.4,将验证码存在session中
2.5,设置图片格式
2.6,读写响应到浏览器
2.5 登录
1,业务层开发
1.1,验证账号空值处理
1.2,验证密码空值处理
1.3,根据用户名查询,为空,返回错误
1.4,验证状态是否激活
1.5,验证密码是否正确
1.6,生成登录凭证
2,controller开发
2.1,校验验证码
2.2,设置超时时间
2.3,调用业务层login方法
2.4,判断map中是否包含ticket,存在cookie
2.5,其他把错误的存在model中
2.6 退出
1,根据ticket修改状态为1退出
2,controller层,用@CookieValue注解获取cookie
,实现退出功能
2.7 显示登录信息
1,需要实现拦截器HandlerInterceptor
2,需要编写获取cookie的工具类
3,需要用到ThreadLocal
持有用户信息
4,拦截器实现preHandle,postHandle,afterCompletion
三个方法
4.1,preHandle
在请求之前获取用户信息,存在ThreadLocal
中
4.2,postHandle
在模板渲染之前获取用户信息,存在域对象中
4.3,afterCompletion
模板调用完之后清理掉用户信息hostHolser
5,配置拦截器需要实现WebMvcConfigurer
,编写方法传参数InterceptorRegistry(拦截器注册表)
,调用addInterceptor(添加拦截器)
,excludePathPatterns(放行路径)一般都是静态资源
,excludePathPatterns(放行)
方法
2.8 设置账号头像
1,上传头像需要注意的是必须是post请求,表单是enctype="multipart/form-data"
,Multipart上传文件
2,业务层根据用户id修改头像
3,controller层编写上传头像
3.1,判断上传头像是否为空,为空,页面显示错误提示
3.2,得到头像的源文件getOriginalFilename()
方法
3.3,截取头像后缀,从.
后面截取,截取返回为空,直接返回页面提示错误信息
3.4,拼接图片文件名
3.5,确定文件存放路径
3.6,Multipart
调用transferTo(文件路径)存文件
3.7,需要重新修改下头像存放的web路径
2.9 获取头像
1,是通过路径获取
2,获取服务器存放的路径
3,需要截取动态获取文件的后缀响应到浏览器
4,读写操作获取头像
2.10 检查登录状态
1,使用注解标记需要拦截的方法
2,拦截器判断
- 判断拦截的是不是方法
- 如果是方法,得到方法的注解
- 如果有我们上面自定义的方法,说明这个方法登录了才能访问
- 如果当前没有登录,return false,拒绝请求,返回登陆页面
3. 开发社区核心功能
3.1 过滤敏感词
3.2 发布帖子
1,需要发送异步请求
2,需要用到fastjson
传输json
数据
3,需要新建一个工具类CommunityUtil
4,业务层
4.1,转义HTML标记HtmlUtils.htmlEscape()方法,
4.2,过滤敏感词
5,controller层
5.1, 判断当前是否登录,没登陆返回code:403
5.1,登录返回成功的状态码和提示消息
6,编写异步请求代码
3.3 帖子详情
1,根据路径传的帖子id,点击进去进去帖子详情页面,controller层调用根据id查询帖子方法,
2,也需要查询到用户作者装进模板中
3.4 显示评论
1,数据层
1.1,查询评论列表方法对应的sql语句
- 根据entity_type和entity_id条件查询
1.2,查询评论的总数方法对的sql语句
2,业务层
直接调用mapper
3,controller
3.1,分页
3.2,先编写给帖子做评论
3.3,需要把帖子评论装进map封装到List集合中
- 判断下查询的帖子评论帖子列表是否为空,之后遍历评论帖子列表
- 构建map集合,添加评论,作者
3.4,回复列表,给评论的回复
- 判断下查询的评论的回复列表是否为空,之后遍历评论的回复列表
- 构建map,装回复,装作者
- 需要查询回复目标用户的数据,装map
- 最后装进集合中
3.5,在把给评论的评论集合装进map中
3.6,查询到给评论回复的数量装进map
3.7,最后添加到集合中
3.7,然后再把集合装进model中
3.5 显示评论
1,业务层需要添加评论的帖子数据的时候,然后再更新帖子的总数量,需要用到事务
2,表现层需要路径携带帖子id添加回帖,把回帖的数据保存在评论表中,最后重定向帖子详情页面
3.6 私信列表
1,数据层
1.1,查询当前用户的会话列表,针对每个会话只返回一条最新的私信
- sql语句需要查询出message最大的id显示那条对话,需要注意的是发送者和接受者都有可能在上面所以sql语句需要加or连接做判断,最大id做子查询查询出那一条数据。
1.2,查询当前用户的会话数量
- sql语句需要根据会话分组查询到每个人和当前用户对话的数量,和上述列表一样,改成最大计算所有和当前用户私信的数量
1.3,查询某个会话所包含的私信列表.
- 点击进去sql语句是根据会话id查询多少条数据的
1.4,查询某个会话所包含的私信数量
- 直接count(id)
1.5,查询未读私信的数量
- sql语句状态等于0未读,需要动态拼接
conversationId,因为需要查询显示一个用户对话的未读数量
2,业务层需要注意的是就添加私信要过滤,转义html
3,controller层
3.1,设置分页信息
3.2,会话列表
-
调用业务层方法查询
-
把查询到的数据封装到map中,最后在封装到List集合中
-
遍历查询到的私信列表,判断是否为空
-
构建map,装message,装私信数量,需要调用根据会话id查询用对话数量,装
未读私信数量,**注:
**还需要显示对话用户头像,
需要判断如果当前用户是发送者,接受者
就是to,反之亦然,也需要得到用户id查询用户数据,存到map中
- 最后存在model
3.3,查询未读消息的数量总数,存在model
3.7 私信详情
1,路径传参,根据会话id点进去会话私信详情
2,设置分页
3,私信列表
3.1,查询到私信详情的列表,封装map,最后封装List集合
- map中存私信,存私信的用户
3.2,
私信目标需要编写一个方法分割会话id,判断目标人的名字
3.8 发送私信
1,
mapper层修改消息状态需要用到遍历sql语句,遍历id
2,业务层需要注意的是添加的时候需要转义html和过滤敏感词
3,controller编写发送私信异步请求
3.1,先查询发送的目标用户是否存在,不存在异步发送失败的消息
3.1,把发送的消息存在数据库
- 存当前用户
- 存目标用户
- 存会话id,如果from小于to就小在前,反之亦然
- 存发送主题
- 存当前日期
- 最后调用业务层保存
3.1,最后返回异步请求,状态码0
3.9 设置已读
1,额外编写一个方法,获取私信列表,然后再得到id,然后再私信详情列表调用根据id集合修改状态已读
3.10 统一处理异常
1,编写异常处理类
@ControllerAdvicea (nnotations = Controller.class)
用来修饰类,对controller全局配置@ExceptionHandler
用于修改方法,该方法会在controller出现异常会调用,用于处理捕获的异常
2,需要编写一个异常跳转的路径,用来显示500异常页面
3,异常处理类编写
-
异常日志记录
-
异常是异步异常,还有普通请求异常,需要判断异步
-
异步判断获取请求头中是否有
XMLHttpRequest
,设置响应类型response.setContentType("application/plain;charset=utf-8");
-
普通请求重定向500页面
3.11 统一处理日志
1,用到aop
2,创建切面类注解@Aspect
3,切点方法需要标注的类@Pointcut
4,使用前置通知统一记录日志@Before
-
RequestContextHolder.getRequestAttributes();
得到request对象 -
request得到ip地址
request.getRemoteHost()得到
-
得到当前时间
SimpleDateFormat格式化日期时间
-
JoinPoint
得到类名方法名做拼接 -
最后做拼接打印日志,拼接这种格式
用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()]
4.Redis
4.1 验证码缓存
1,登录验证码key用UUID
自动生成,通过cookie传递到浏览器
2,登录的时候通过注解@CookieValue("kaptchaOwner")
获取redis的key,然后通过reids获取验证码
4.2 使用redis缓存登录凭证
1,key是那个UUID自动生成的
2,value是那个LoginTicket
,里面有登录凭证,用户id,状态,过期时间
4.3 根据id查询用户
1,直接先查询缓存,缓存中没有查询数据库,然后更新到缓存中
2,更新操作需要删除缓存
4.4 点赞
1,构建点赞的key,实体类型,实体id作为key,用户id作为value,选择无序集合
4.5 我收到的赞
2,构建统计点赞数量,用户id作为key,需要用到事务和点赞一起统计
4.5 关注和取消关注
1,我关注的人,key是自己用户的id和是实体类型,value是关注的人的用户id
2,他那边看是,我是粉丝,key应该是他登录的用户id,加实体的类型,value是自己用户Id
业务编写
1,使用数据类型是Zset
有序集合,当前时间作为分数
2,需要用到事务,需要存储关注者,和那边被几个人关注
3,统计关注实体数量的方法
4,查询实体的粉丝的数量方法
5,查询是否已关注改实体,根据key,value查询有没有分数
表现层编写
1,follow,unfollow关注和取消关注编写,发送异步请求
2,个人主页调用统计粉丝数量和关注者方法,关注的状态方法
4.6 关注列表和粉丝列表
业务层编写
1,关注列表,分页查询,按索引分页,最后返回List集合,用户,时间
- 查询出来的是用户id,构建List集合存map,遍历返回的集合,查询出用户存在map中,也需要把分数存在map中,因为需要排序,redis查询出分数,然后转为时间
2,粉丝列表,分页查询,按索引分页,最后返回List集合,用户,时间
- 上述一样。。
表现层编写
关注列表
1,根据用户id路径,点击进去查看
2,需要注意一点,点进去查看列表对方也要显示关注未关注的状态,所以需要查询出来,显示
5. kafka
5.1 发布系统通知
定义事件类
set方法改一下,有返回值为Event,可以连续.set进行处理,比全参构造器更灵活。
setData改一下只传key和value,调用更方便
定义生产者
1,注入kafkaTemplate
2,调用kafkaTemplate.send(主题,内容)
定义消费者
1,消费的主题直接定义三个
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
2,调用ConsumerRecord
的value()方法消费主题
3,消费者里面需要把Event内容存到数据库中
- fromId存系统用户
- toId存实体用户id
- ConversationId存主题
- setCreateTime存当前时间
- content存用户id,实体类型,实体id,存event类中map里存的值
评论后,发布通知
1,发布评论后触发通知
- 实例化Event
- 事件主题:评论
- 当前用户
- 类评论的类型
- 评论的id
- 帖子id
- 需要判断,当前评论的是帖子还是评论,如果是帖子话根据帖子id查询到帖子,根据帖子查到用户id,存在
EntityUserId
,如果是评论上述一样。 - 最后调用生产者触发事件
点赞后,发布通知
1,需要注意的是点赞的触发取消点赞不发布通知
2,这个需要注意的话参数传一个帖子id,需要把帖子id存进去
关注后,发布通知
上述类似。。。。。
出错
测试时报错,拦截器报错,HttpServletRequest request = attributes.getRequest();
中的attributes可能为空。之前没有消费者,所有对service的访问都通过controller实现,消费者调用service,它没有requst,得不到,简单解决就是如果null直接return
5.2 显示系统通知
5.2.1 通知列表
SQL语句写法和消息通知一样
查询评论类通知
1,根据页面显示,需要把查询到的消息数据存到map中,取出之前存在context中的用户id,实体类型,实体id,帖子id作为页面显示,需要注意因为content存的是转移字符和json字符串,需要解析转意字符用到方法HtmlUtils.htmlUnescape()
,在转为map对象。
2,查询通知几条通知,调用业务层查询
3,查询评论未读的数量
查询点赞通知
1,一样
查询关注通知
2,去掉帖子id,因为不需要链接到帖子详情
5.2.2 通知详情
1,sql语句是查询每个主题通知的列表
2,表现层编写
- 根据主题路径点击进去
- 设置分页
- 调用查询每个主题通知的列表
- 构建List集合存map类型,和通知列表一样取出,然后存在List中,最后存在model
- 设置已读
- 查看状态情况,然后调用业务把状态修改成已读
5.2.3 通知详情
1,需要用到拦截器,因为只有登录才能看见
- 在模板没渲染之后,统计出用户未读消息+系统未读消息=总的未读消息
6. Elasticsearch
6.1
7. security
7.1 demo测试
1,userService实现UserDetailsService,重写UserDetails方法,主要作用查询数据库
2,User实现UserDetails,是UserDetails返回值,主要封装用户,权限
3,securityConfig继承WebSecurityConfigurerAdapter
-
重写
configure(WebSecurity web)
,忽略静态资源的访问 -
重写
configure(AuthenticationManagerBuilder auth)
- 自定义认证规则
(1)
AuthenticationProvider: ProviderManager
持有一组AuthenticationProvider
,每个AuthenticationProvider
负责一种认证(2)
AuthenticationProvider
实现两个方法,认证和当前的AuthenticationProvider
支持哪种类型的认证(3)Authentication接口获取用户名和密码进行认证,
接口实现类UsernamePasswordAuthenticationToken
-
重写
configure(HttpSecurity http)
各种相关的配置- 登录配置
- 登陆成功处理器
- 登录失败处理器
- 退出配置
- 退出成功处理器
- 授权配置
- Filter配置
- 请求最前面获取验证码信息
- 记住我配置
- 异常处理
- 没有登录
- 权限不足
- 登录配置
7.2 权限控制
1,引入依赖
2,废除原有拦截器
3,SecurityConfig
继承WebSecurityConfigurerAdapter
4,重写configure(WebSecurity web)
忽略对静态资源的拦截
4,重写configure(HttpSecurity http)
进行授权
http.authorizeRequests()
进行授权
-
.antMatchers
:登陆后可访问路径 -
.hasAnyAuthority
:可以访问的权限 -
.anyRequest().permitAll()
:其他请求都允许 -
http.exceptionHandling()
:没有登录,权限不足处理- 没有登录 authenticationEntryPoint
(1)判断是否异步请求:String xRequestedWith = request.getHeader(“x-requested-with”);其中xRequestedWith的值为"XMLHttpRequest"
是异步请求,返回一个json字符串,提示还没有登陆,不是异步请求就重定向到登陆页面- 权限不足 accessDeniedHandler
(2)和上述一样,异步返回提示,普通请求重定向错误页面
- 退出
Security底层默认会拦截/logout请求,进行退出处理.
覆盖它默认的逻辑,才能执行我们自己的退出代码.
5,UserService
增加用户权限
- 增加方法
getAuthorities
,传入userId- 根据用户id查询到用户
- 实例化list集合,类型
GrantedAuthority
- 重写
getAuthority
,user.getType()
添加到list集合
6,在拦截器获取用户认证的结果,因为只有登录过后才能认证授权
,存在SecurityContextHolder,请求结束做下清理,退出接口做下清理
7.3 置顶 加精 删除
1,置顶
1,发送异步请求
2,调用业务层根据id修改为置顶帖
3,触发发帖事件
4,消费发帖事件存到Elasticsearch
中
2,加精
1,状态修改成1,触发加精事件,消费发帖事件存到Elasticsearch
中
3,删除
1,把状态修改成2,触发删除事件
2,消费删除事件,从Elasticsearch
删除
4,权限分配
1,securityConfig配置
8. 网站数据统计
1,统计uv,key当前日期,value是ip
2,统计dau,key是当前日期,value是用户id
统计指定日期范围内不太明白
3,拦截器,请求之前记录
9. Spring Quartz
1,Scheduler
接口核心的调度工具
2,Job
接口定义任务
3,JobDetail
接口配置Job的任务详情
4,Trigger(触发器)
配置Job什么时候运行
配置好之后,读取配置信息读取到数据库中,读取表来执行任务,配置只在第一次初始化数据后,不在使用,直接读取数据库中的表
实现步骤
1,创建类继承Job
2,QuartzConfig
配置JobDetail
, 配置Trigger
3,配置Quartz,会读取到数据库中,不配置的话默认读取到内存
9.1 热帖排行
- 构建帖子分数key
- 需要算分的地方
- 新增帖子需要给他一个初始的分数,缓存到redis中
- 不适合放在队列里面,避免重复计算,放在redis中,选用set类型,去重作用
- 加精的时候需要算分
- 评论的时候需要算分
- 点赞的时候需要算分
- 注意:只有给帖子点赞的时候才计算分数
- 编写定时任务类,实现JOB
-
需要初始化牛客的成立时间
- 在静态代码块初始化时间的格式
-
重写execute方法
-
得到key的集合
-
如果redis没有值,打印日志,任务取消,么有需要刷新的帖子
-
接着任务开始,正在刷新帖子分数
-
循环弹出帖子,新加一个
刷新
(refresh)帖子方法-
刷新帖子方法根据id查询出帖子,如果不存在,打印错日志,直接返回空
-
判断是否精华
-
计算评论数量
-
计算点赞数量
-
最后计算权重
- 公式:( 精华+75)+ 评论 * 10 + 点赞 * 2
-
计算分数 = 帖子权重 + 距离天数;
- 因为权重有可能是负数,计算log以10为底时候为负数,
Math.max(w,1),如果w为负数的时候返回1,返回区间最大的数
,之后在加上距离天数(帖子创建时间-成立时间 )
- 因为权重有可能是负数,计算log以10为底时候为负数,
-
更新帖子分数
-
同步搜索数据
- 因为实体post分数是之前的,需要更新下分数,在存在es中
-
-
- 配置定时任务
- 数据展现
-
修改数据访问层
selectDiscussPosts
方法增加一个参数-
之前是类型,时间排序,现在增加一个参数(
orderMode
),如果值是0,还是原来的排序,如果传入的参数是1,按照热度来排序(利用动态sql,传入1的时候需要增加分数的排序)
-
注:alt + F7 查看类的调用
-
HomeController中,直接访问页面是默认的排序,页面需要传入默认值0,用
@RequestParam(name = "orderMode",defaultValue = "0")
标注orderMode
-
orderMode是用?传过来的,因为是get请求,需要拼接在
"/index?orderMode "+orderMode中
,最后把orderMode
存在模板中
-
- 页面展现
- index.html修改
- 如果order等于0最新
- 如果order等于1最热
10. 生成长图
-
配置环境变量
-
生成
pdf
命令
cmd: wkhtmltopdf https://www.baidu.com d:/wkhtmltopdf/data/wk-pdfs/1.pdf
- 生成
image
命令
cmd: wkhtmltoimage https://www.nowcoder.com d:/wkhtmltopdf/data/wk-images/1.png
- 压缩图片百分之75
cmd: wkhtmltoimage --quality 75 https://www.nowcoder.com d:/wkhtmltopdf/data/wk-images/1.png
- java代码调用需要全路径
java: d:/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://www.nowcoder.com d:/wkhtmltopdf/data/wk-images/2.png
- 配置wk文件
#wk
wk.image.command=d:/wkhtmltopdf/bin/wkhtmltoimage
wk.image.storage=d:/wkhtmltopdf/data/wk-images
- 如果路径不存在,直接自动创建目录
- 需要创建一个配置类,这个配置类不加载bean,只需要调用初始化方法一次,创建路径
- Controller生成长图,用
kafka
异步实现
-
需要创建一个
分享(share)
主题 -
kafka Event存放主题,路径地址,文件名,文件格式
-
返回的文件路径存放在map,是动态的
-
编写消费分享事件
- 命令拼接,执行命令,打日志
- 获取长图
- 输入输出流读取
11.上传云服务器
-
七牛云,选择对象存储
-
创建两个对象空间,头像,分享的图片
-
配置七牛云秘钥,域名
-
引入七牛云依赖
-
上传头像获取头像废弃
- 在打开头像设置编写上传七牛云
- 上传文件名称
- 设置响应信息
- 生成上传凭证
- 更新头像图片路径
- 编写一个新的方法异步请求
- 获取用户文件名,拼接访问路径
- 修改setting.html上传到七牛云
http(s)😕/upload-z1.qiniup.com
- 分享长图上传到七牛云
- 修改文件路径
- 启用定时器上传到七牛云
- EventConsumer编写一个类继承Runnable
- 属性
- 上传的文件名称
- 文件后缀
- 启动任务的返回值
- 开始时间(主要计算时间上传30秒就停止)
- 初始化时间,在构造器初始化起始时间
- 上传次数(主要计算上传3次就停止)
- run()方法
- 如果时间超过三十秒强制停止
- 上传次数超过等于三次停止
- 判断本地图片路径是否存在
- 打印日志记录上传凭证,上传次数
- 设置响应信息
- 生成上传凭证
- 指定上传机房