1 Star 0 Fork 5

刚刚好 / community

forked from 李莲花 / community 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

community

介绍

这是一款基于Springboot的牛客论坛项目,项目实现功能:包括注册,登录,帖子,多级评论,点赞,关注,私信,搜索,系统通知,网站数据统计,热帖排行,敏感词过滤,全局异常处理,统一日志记录等。使用技术栈:SpringBoot,Springmvc,Mybatis,Spring Email,Spring Security,Mysql,redis,ElasticSearch,Kafka,Quartz

效果图

输入图片说明 输入图片说明 输入图片说明 输入图片说明 输入图片说明 输入图片说明 输入图片说明 输入图片说明 输入图片说明

软件架构

软件架构说明

安装教程

  1. xxxx
  2. xxxx
  3. xxxx

使用说明

  1. xxxx
  2. xxxx
  3. xxxx

参与贡献

  1. Fork 本仓库
  2. 新建 Feat_xxx 分支
  3. 提交代码
  4. 新建 Pull Request

特技

  1. 使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md
  2. Gitee 官方博客 blog.gitee.com
  3. 你可以 https://gitee.com/explore 这个地址来了解 Gitee 上的优秀开源项目
  4. GVP 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
  5. Gitee 官方提供的使用手册 https://gitee.com/help
  6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 https://gitee.com/gitee-stars/

User UserMapper user-mapper.xml MapperTests UserService DiscussPost DiscussPostMapper discusspost-mapper.xml MapperTests DiscussPostService

发送邮箱 要启用客户端SMTP服务,导入jar包,配置邮箱参数,使用JavaMailSender发送邮件,使用Thymeleaf发送HTML邮件 MailClient MailTests demo.html

注册功能 :提交注册数据(通过表单提交数据;服务端验证数据是否存在,邮箱是否已注册;服务端发送激活邮件),激活注册账号(点击邮件链接,访问服务端的激活服务)register.html CommunityUtil(生成随机字符串,MD5加密)UserService 写register()方法,实现密码加密,生成激活码,获取随机头像,激活邮件 activation.html LoginController 写register()方法, CommunityConstant 常量接口 让UserService实现这个接口 写activation()方法,让LoginController实现这个接口 写activation()方法, 生成验证码:KaptchaConfig 配置类 LoginController写getKaptcha()方法

登录功能 :验证账号,密码,验证码;成功时,生成登录凭证,发放给客户端;失败时,跳转回登录页 LoginTicket LoginTicketMapper(这里使用注解实现dao(代码简单时可用),上面都是使用xml文件)MapperTests UserService 写login()方法 LoginController 写login()方法 在CommunityConstant常量接口中加相应的常量 login.html

退出登录时将登录凭证改为失效状态 UserService 写logout()方法 LoginController 写logout()方法 显示登陆信息: 拦截器 :在请求开始时查询登录用户,在本次请求中持有用户数据,在模板视图上显示用户数据,在请求结束时清理用户数据 AlphaInterceptor 拦截器 WebMvcConfig 拦截器配置文件 UserService 写findLoginTicket()方法 HostHolder 持有用户信息,用于代替session对象 LoginTicketInterceptor 拦截器

账号设置: 上传文件 setting.html 配置资源上传路径 UserService 写updateHeader()方法 UserController 写uploadHeader()方法和getHeader()方法 检查登录状态 使用拦截器在方法前标注自定义注解,拦截所有请求,只处理带有该注解的方法。在用户未登录时无论如何也不能访问到一些功能 LoginRequired 自定义注解 (拦截器拦截的注解)LoginRequiredInterceptor拦截器 WebMvcConfig 拦截器配置文件

邮箱注册

对前台传来的注册表单数据进行判重判空和数据库匹配后,如果注册成功,将用户信息存入数据库:注意:密码的存储是经过加盐和md5加密的,防止密码泄露。数据库存储了该用户的盐和加密的密码。然后发送激活邮件,用户点击链接则激活账号。 mailClient工具类封装了JavaMailSender进行邮箱激活,需要在配置文件中进行邮箱SMTP服务的配置。这里用子线程去发送邮件,防止卡顿时间过长。 邮箱里的激活链接即是通过"/activation/{userId}/{code}"访问路径修改用户的status字段,使其可用。 用户注册 **

登录**

登录时检查信息正确性的逻辑和注册时基本一致,需要对账号进行非空、存在和激活的判断,对验证码进行判断,对密码进行非空和正确的判断,以及是否有rememberMe。登陆成功后生成LoginTicket存入数据库,记录了用户ID、ticket、过期时间等,ticket字段会被放入cookie中。 LoginTicket登录凭证之所以存入数据库,是考虑到了session在分布式环境下请求分发导致的会话状态无法保持的问题。 ** 验证码** 使用google提供的Kaptcha实现验证码,登录时要检查验证码,逻辑如下: 生成图片时存入session(后面用Redis优化):登录时从session中取值和表单值进行比对即可

状态保持 登录后需要进行状态保持,可以用前面提到的登录凭证+Interceptor+ThreadLocal实现。 拦截器在preHandle时检查cookies中是否有有效ticket,有的话就在当前请求中持有用户信息。HostHolder类封装了ThreadLocal< User >,ThreadLocal的目的:Tomcat服务器会使用独立线程去处理每个请求,因此需要隔离多请求多用户,防止信息混乱。 拦截器在postHandle时若发现该次请求中有用户信息,需要在 modelAndView中添加用户信息以保持状态。 在afterCompletion清空信息即可。

附:登录模块逻辑: 进入登录界面,随机生成一个字符串来标识这个将要登录的用户,将这个字符串短暂的存入 Cookie(60 秒); 动态生成验证码,并将验证码及标识该用户的字符串短暂存入 Redis(60 秒); 为登录成功(验证用户名、密码、验证码)的用户,随机生成登录凭证且设置状态为有效,并将登录凭证及其状态等信息永久存入 Redis,再在 Cookie 中存一份登录凭证; 使用拦截器在所有的请求执行之前,从 Cookie 中获取登录凭证,只要 Redis 中该凭证有效并在有效期内,本次请求就会一直持有该用户信息(使用 ThreadLocal 持有用户信息,保证多台服务器上用户的登录状态同步); 勾选记住我,则延长 Cookie 中登录凭证的有效时间; 用户登出,将凭证状态设为无效,并更新 Redis 中该登录凭证的相关信息 用户登录

设置头像

File文件上传,修改用户的头像链接使其可以通过url访问头像图片。图片存放位置可暂存本地,后改为云服务器。 修改头像(异步请求) 将用户选择的头像图片文件上传至七牛云服务器 修改密码 修改头像

简单的权限管理 设置页面和修改头像请求显然必须登录才能使用,可以通过注解进行简单的权限管理: 自定义@LoginRequired注解类,并添加到需要权限的方法上,然后通过拦截器进行判定。在访问当前方法时若有该注解则必须是已登录状态

分页显示所有的帖子

支持按照 “发帖时间” 显示 支持按照 “热度排行” 显示(Spring Quartz) 将热帖列表和所有帖子的总数存入本地缓存 Caffeine(利用分布式定时任务 Spring Quartz 每隔一段时间就刷新计算帖子的热度/分数 — 见下文,而 Caffeine 里的数据更新不用我们操心,它天生就会自动的更新它拥有的数据,给它一个初始化方法就完事儿)

分页 通过Page类封装分页逻辑:前台传来的current等参数通过controller的Page类参数进行封装,从而实现页面跳转。该模块可用于其他地方大量复用。 帖子模块

发布帖子 (异步请求)

发布帖子(过滤敏感词),将其存入 MySQL 异步发送请求(使用ajax,网页能够将增量更新呈现在页面上,而不需要刷新整个页面),以通过提示框展示提示信息,发帖后台就是普通的crud。 发布帖子

敏感词过滤

基于Trie前缀树结构,查找效率高(消耗内存大),常应用于字符串检索,词频统计,字符串排序等。该模块可用于帖子、评论、私信等。 敏感词过滤器: 定义前缀树 根据敏感词,初始化前缀树 编写过滤敏感词的方法 敏感词过滤

显示评论

评论部分前端的名称显示有些缺陷,有兴趣的小伙伴欢迎提 PR 解决 ~ 关于评论模块需要注意的就是评论表的设计,把握其中字段的含义,才能透彻了解这个功能的逻辑。 评论 Comment 的目标类型(帖子,评论) entityType 和 entityId 以及对哪个用户进行评论/回复 targetId 是由前端传递给 DiscussPostController 的 显示评论 一个帖子的详情页需要封装的信息大概如下: 输入图片说明

添加评论 (事务管理)

发布对帖子的评论(过滤敏感词),将其存入 MySQL 评论分为对帖子的评论(简称评论)和对评论的评论(简称回复)。 entityType+entityId指示评论的对象(是帖子还是评论,然后具体Id为多少),当entityType为评论,且该条评论为回复时有效,为0表示该条回复评论的是对帖子的评论,非0则是代表该条回复评论的是回复,另外注意,帖子entity中有个关于评论的冗余数据,因此有新评论产生时需要通过事务进行更新: 添加评论

私信列表

其中conversationId由fromId和toId拼接而成,小Id在前,如111_112,表示111和112之间的私信。status记录私信是否已读,当用户进入私信详情页面时,会更新未读私信状态为已读。 私信列表

发送私信 (异步请求)

采用异步的方式发送私信,发送成功后自动刷新私信列表 查看私信详情时,将显示的私信设置成已读状态 发送私信

统一异常处理

JavaWeb的思想是异常尽量不处理,而是往上层抛给controller去处理。spring对此提供了简单支持,若有4xx或5xx异常,则返回的页面为/error包下对应的4xx.html或5xx.html。但我们有如下需求: 记录错误日志 对于异步/非异步提供友好提示 因此我们可以用@ControllerAdvice注解实现异常处理 统一异常处理

统一记录日志

我们需要知道哪些用户什么时候对什么方法进行了访问,因此最好能提供日志记录,一个解决思路是对于每个方法都进行硬编码记录日志,但这种思路显然违背了开闭原则,不利于扩展和维护。因此我们可以用AOP的思想解决问题,Spring对此提供了友好支持:(AOP面向切面编程,可以进一步提高编程效率) 切入时机可以是pointcut调用前(@Before),pointcut调用后(@After),环绕pointcut(@Around),return后(@AfterReturning),异常后(@AfterThrowing)。这里只需@Before即可。

点赞关注

点赞关注使用较频繁,是通过Redis实现的。我们在配置类中将RedisTemplate<String, Object>注入Spring容器,该Bean需要设置key和value的序列化方式(存入的有可能是对象,采用json格式进行序列化)。

点赞 (异步请求)

点赞用Ajax实现,可对帖子也可对评论点赞,第一次点赞,第二次取消点赞(1:点过赞 0:未点过赞),并显示点赞状态。根据返回的点赞状态和点赞数量进行正确的局部更新。Service方法如下: 其中RedisKeyUtil是一个构造Redis key的工具类,生成的key是由各个字段用冒号隔开的(Redis的惯用key命名方式)。 我们通过set数据结构记录某个实体(如帖子)的点赞用户Id,此外用一个string数据结构记录某个用户获得的赞总数: 这两个数据结构的操作需要用到Redis的事务一并实现。

将点赞相关信息存入 Redis 的数据结构 set 中。其中,key 命名为 like:entity:entityType:entityId,value 即点赞用户的 id。比如 key = like:entity:2:246 value = 11 表示用户 11 对实体类型 2 即评论进行了点赞,该评论的 id 是 246 某个用户的获赞数量对应的存储在 Redis 中的 key 是 like:user:userId,value 就是这个用户的获赞数量 点赞

我收到的赞

重构点赞功能:以用户为key,记录点赞数量,查询点赞数量 输入图片说明

关注 (异步请求)

统计用户的关注数,粉丝数 关注的对象可以是用户、帖子、评论。和点赞的区别在于:key 中需要包含关注者和被关注者这两个变量。 若A关注了B,则A是B的粉丝,B是A得目标

Service层定义follow和unfollow等方法,试举一例: 你可以查看别人的关注列表,并对列表中的用户进行关注。 这里要用zset数据结构,在进行关注的人/粉丝列表显示的时候,可以根据关注时间进行排序显示。

若 A 关注了 B,则 A 是 B 的粉丝 Follower,B 是 A 的目标 Followee 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体(目前只做了关注用户) 将某个用户关注的实体相关信息存储在 Redis 的数据结构 zset 中:key 是 followee:userId:entityType ,对应的 value 是 zset(entityId, now) ,以关注的时间进行排序。比如说 followee:111:3 对应的value (20, 2020-02-03-xxxx),表明用户 111 关注了一个类型为 3 的实体即人(用户),关注的这个实体 id 是 20,关注该实体的时间是 2020-02-03-xxxx 同样的,将某个实体拥有的粉丝相关信息也存储在 Redis 的数据结构 zset 中:key 是 follower:entityType:entityId,对应的 value 是 zset(userId, now),以关注的时间进行排序 关注

关注列表,粉丝列表

输入图片说明

** 缓存优化 **

下面用Redis缓存进行了三处地方的性能优化。 输入图片说明

代替session存储验证码 理由如下: 验证码需要频繁的访问和刷新,对性能要求较高 验证码只需要暂存,不需要长期保存 如未来涉及到分布式部署,能避免session共享的问题。

存储登录凭证 每次对网站的请求都会通过拦截器查询用户的登录凭证,因此考虑到凭证的时效性和经常性,可以改为用Redis存储凭证而不是用数据库存储。

存储用户信息 同上,每次对网站的请求都会通过拦截器获取用户的登录凭证,然后根据凭证获取用户信息以保持登录状态,访问频率非常高。因此对于findUserById这个方法,有必要做Redis缓存: 这样,整个拦截器所调用的方法就不会涉及到数据库了。 但用户信息的存储会涉及到Redis和MySQL的缓存不一致问题,需要解决: 1.优先从缓存中取值 2.取不到时初始化缓存数据 3.数据变更时清除缓存信息 关于缓存不一致问题,有很多解决方法,这里采用的是:当数据变更时,先更新数据库,再删除缓存。 当然无论是先更新数据库还是先删除缓存,都会有并发访问情况下的不一致问题和第二步操作失败的问题。 前一个问题可以采用延迟双删策略来解决。后一个问题可以用重试机制来解决

发送系统通知

用Kafka做消息队列也能对系统进行优化。(Kafaka构建TB级异步消息系统) 输入图片说明 原先Controller层的一些实现逻辑,可以转移到EventConsumer类中实现,享有消息队列异步削峰解耦的优势。例如系统通知的实现,本身和点赞、关注、评论的逻辑关联不强,且这些动作频繁发生,因此可以通过异步实现,提高性能。在例如后面用到的ES数据库的更新也可以用消息队列来实现。 触发事件:

  • 评论后,发布通知
  • 点赞后,发布通知
  • 关注后,发布通知 处理事件:
  • 封装事件对象
  • 开发事件的生产者
  • 开发事件的消费者

发送系统通知 **

显示系统通知**

至于系统消息的查看、列表等实现,则基本与私信的实现差不多。(数据库中from_id为1表示这不是普通私信而是系统通知) 显示评论,点赞,关注三种类型的通知 在页面头部显示所有未读消息的数量,也显示各种类型主题未读消息数量 显示系统通知

搜索

Elasticsearch目前性能最好,最流行的分布式搜索引擎。支持对各种类型数据的检索,搜索速度非常快,可以提供实时的搜索服务,每秒可以处理PB级海量数据 Elasticsearch相当于特殊的数据库,搜索就是对这个数据库的搜索。 ElasticsearchService需要完成三个方法:save、delete和search,重点是search: 发布事件: 发布帖子时,通过消息队列将帖子异步地提交到 Elasticsearch 服务器 为帖子增加评论时,通过消息队列将帖子异步地提交到 Elasticsearch 服务器 搜索服务: 将帖子保存到Elasticsearch服务器 从 Elasticsearch 服务器搜索帖子 从 Elasticsearch 服务器删除帖子(当帖子从数据库中被删除时) 显示搜索结果: 在控制器中处理搜索请求,在HTML上显示搜索结果 搜索 类似的,置顶、加精也会触发发帖事件,就不再图里面画出来了。

权限控制

Spring Security是一个专注于为java应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松拓展以满足自定义的需求。可以防止各种攻击,如会话固定攻击,点击劫持,csrf攻击等 废弃了之前采用拦截器实现的登录检查,使用 Spring Security框架来进行统一认证授权管理。Security底层原理蛮复杂的,我们这里对其进行简单的使用。 而用户认证方面,由于我们采用自定义的认证方式,因此无需采用Security提供的方式,但我们认证信息仍需要存到SecurityContext里,拦截器需要改进 认证信息会在preHandle加入,afterafterCompletion处删除,logout时也会删除。 关于认证信息何时删除的思考: 每次在afterCompletion时删除: Security是基于Filter的,如果每次在afterCompletion时删除,那么下次请求时首先到达Filter,由于没有认证信息,会被判定权限不够,直接跳转到登录页面,即使已经登录。 在afterCompletion时不删除,只在logout时删除: 如果这样虽然能保证权限与请求匹配,但是由于登录凭证会过期,用户信息会被清除,但认证信息却不会被清除,用户即使没登录也能访问,显然不合常理。 因此正确做法为afterafterCompletion处如果没有用户信息就删除认证信息,logout时也删除。

置顶加精删除 (异步请求)

版主可以看到置顶,加精操作;管理员可以看到删除操作;点击置顶修改帖子的类型,点击加精,删除修改帖子的状态 Thymeleaf有对Security的支持,可以从SecurityContext从获得权限信息。从而赋予用户不同的权限(置顶、加精、删除) 输入图片说明 置顶加精删除

网站数据统计

输入图片说明 独立访客 UV

  • 存入 Redis 的 HyperLogLog
  • 支持单日查询和区间日期查询

使用Redis的HyperLogLog数据结构去实现,该数据结构的特点是占用内存很小,但会损失一定的统计精度。 使用拦截器,游客每次访问时Redis计入该ip。 统计区间UV:整理日期范围内的key,合并数据(union去重生成新数据),返回统计结果 日活跃用户 DAU

  • 存入 Redis 的 Bitmap
  • 支持单日查询和区间日期查询

类似UV统计,只是DAU统计操作的是bitmap:统计区间内的DAU,整理日期范围内的key,or运算 权限管理(Spring Security)

  • 只有管理员可以查看网站数据统计 输入图片说明 网站数据统计

热帖排行

使用Quartz实现热帖排行和更新,相比 JDK 的 ScheduledExecutorService 和 Spring 的 ThreadPoolTaskScheduler 的优势: Quartz 实现定时任务所依赖的参数是保存在数据库中,数据库只有一份,所以不会冲突。 而 ScheduledExecutorService 和 ThreadPoolTaskScheduler 是基于内存的,在分布式环境中,多台服务器会重复执行定时任务,产生冲突。 热帖排行和更新的实现逻辑如下: 帖子的score字段计算方法自定义为log(精华分+评论数 * 10+点赞数 * 2+收藏数*2)+(发布时间-牛客纪元)。 一旦涉及到上述score会变化的操作,如帖子被设为精华,或帖子有新的评论等,帖子id会被放入Redis的set中。 每隔一段时间执行定时任务,会从set中pop帖子id出来进行分数的刷新。 每次发生点赞(给帖子点赞)、评论(给帖子评论)、加精的时候,就将这些帖子信息存入缓存 Redis 中,然后通过分布式的定时任务 Spring Quartz,每隔一段时间就从缓存中取出这些帖子进行计算分数。 帖子分数/热度计算公式:分数(热度) = 权重 + 发帖距离天数

 private void refresh(int postId) {
        DiscussPost post = discussPostService.findDiscussPostById(postId);

        //算分的时候发现被管理员删了
        if(post == null) {
            logger.error("待刷新帖子不存在: id = " + postId);
            return;
        }

        //是否精华
        boolean wonderful = post.getStatus() == 1;
        //评论数量
        int commentCount = post.getCommentCount();
        //点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);

        //计算权重
        double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
        //分数 = 权重 + 距离天数
        double score =
                Math.log10(Math.max(w, 1)) + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);

        //更新帖子分数
        discussPostService.updateScore(postId, score);
        //同步搜索数据
        post.setScore(score);
        elasticSearchService.saveDiscussPost(post);
    }

热帖页面根据帖子的score进行排序。 热帖排行

生成长图

使用wkhtmltopdf将牛客网html转换成pdf

将文件上传至云服务器

头像上传到七牛云进行存储,spring对此提供了支持。

优化网站性能

输入图片说明 输入图片说明 在实际的部署中,服务器缓存由于就在服务器本机内,因此对性能的提升相比Redis更高。 对于热帖等与用户状态无关的内容可以存到本地缓存中。本项目用Caffeine实现,主要缓存帖子列表和帖子总数。 需要定义两个缓存管理器,一个针对热帖列表,一个针对帖子总数,可以通过@PostConstruct初始化缓存 用JMeter做下小测试,30w数据的情况下,性能有数倍到数十倍的提升。

压力测试:

使用JMeter工具做压力测试,先在数据库里准备大量数据;在测试方法中比如我初始化30000条数据, 先进行不加缓存测试 双击启动bin目录下的jmeter.bat 添加-》线程-》线程组 填写线程名称,线程数,循环次数,持续时间等 添加-》取样器-》Http请求 填写服务器名称(ip),端口号,http请求方式,路径,编码类型等 添加-》定时器-》统一随机定时器 填写线程延迟属性(ms) 添加-》监听器-》聚合报告 在聚合报告看压力测试结果:主要看吞吐量那一栏(多测试几次,且要保证异常为0%) 然后加缓存进行测试 启用缓存,与上述一样进行压力测试,可以发现有缓存的吞吐量较高

项目部署

需要修改一些路径以适配linux环境。为方便切换,可以使用两套配置文件。Spring允许项目中存在多套配置文件 (比如:开发时一套,部署时一套,测试时一套),通过一个开关来启用哪一套配置文件

整个项目部署到阿里云ECS上(2cpu/4g/CentOS),部署步骤为:

下载相关包:jdk,maven,MySQL、Redis、Kafka、Elasticsearch,Wkhtmltopdf,Tomcat,Nginx。 处理项目: 把项目部署到Tomcat下:Build -》Build Artifact-》community:war->Build,项目被打成war包丢到target包下, 将war包上传到tomcat的webapps文件夹中,部署成功。

空文件

简介

这是一款基于Springboot的牛客论坛项目,项目实现功能:包括注册,登录,帖子,多级评论,点赞,关注,私信,搜索,系统通知,网站数据统计,热帖排行,敏感词过滤,全局异常处理,统一日志记录等。使用技术栈:SpringBoot,Springmvc,Mybatis,Spring Email,Spring Security,Mysql,redis,ElasticSearch,Kafka,Quartz 展开 收起
Java
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/min_gang/community.git
git@gitee.com:min_gang/community.git
min_gang
community
community
master

搜索帮助