寒假前半个月到现在差不多一个多月,断断续续做完了这个项目,现在终于可以开源出来了,我的想法是为这个项目编写一套完整的教程,包括技术选型分析、架构分析、业务逻辑分析、核心技术点分析、常见面试题等。不过说实话,这里面涉及的一些技术我仍然停留在仅仅是会用的阶段,当然,后面我会不断学习,了解透彻这些技术的底层原理,不断完善这套教程。
博主东南大学硕士在读,寒假前半个月到现在差不多一个多月,断断续续做完了这个项目,现在终于可以开源出来了,我的想法是为这个项目编写一套完整的教程,包括技术选型分析、架构分析、业务逻辑分析、核心技术点分析、常见面试题等。不过说实话,这里面涉及的一些技术我仍然停留在仅仅是会用的阶段,当然,后面我会不断学习,了解透彻这些技术的底层原理,不断完善这套教程。
Echo 是一套前后端不分离的开源社区系统,基于目前主流 Java Web 技术栈(SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ...),并提供详细的开发文档和配套教程。包含帖子、评论、私信、系统通知、点赞、关注、搜索、用户设置、数据统计等模块。
源码链接:已托管在 Github 和 Gitee:
Github:https://github.com/Veal98/Echo
Gitee:https://gitee.com/veal98/Echo
在线体验:项目已经部署到腾讯云服务器,各位小伙伴们可直接线上体验:http://1.15.127.74/。已内置三种不同身份的用户:
username | password | 特殊权限 | |
---|---|---|---|
管理员 | admin | admin | 数据统计、删除帖子 |
版主 | master | master | 置顶帖子、加精帖子 |
普通用户 | user | user |
文档地址:文档通过 Docsify + Gitee Pages 生成,国内访问速度较快,在线访问地址:https://veal98.gitee.io/echo
后端:
Spring
Spring Boot 2.1.5 RELEASE
Spring MVC
ORM:MyBatis
数据库:MySQL 5.7
分布式缓存:Redis
本地缓存:Caffeine
消息队列:Kafka 2.13-2.7.0
搜索引擎:Elasticsearch 6.4.3
安全:Spring Security
邮件任务:Spring Mail
分布式定时任务:Spring Quartz
日志:SLF4J(日志接口) + Logback(日志实现)
前端:
Thymeleaf
Bootstrap 4.x
Jquery
Ajax
操作系统:Windows 10
构建工具:Apache Maven
集成开发工具:Intellij IDEA
应用服务器:Apache Tomcat
接口测试工具:Postman
压力测试工具:Apache JMeter
版本控制工具:Git
Java 版本:8
首页:
登录页:
帖子详情页:
个人主页:
朋友私信页:
私信详情页:
系统通知页:
通知详情页:
账号设置页:
数据统计页:
搜索详情页:
用户注册成功,将用户信息存入 MySQL,但此时该用户状态为未激活
向用户发送激活邮件,用户点击链接则激活账号(Spring Mail)
进入登录界面,动态生成验证码,并将验证码短暂存入 Redis(60 秒)
用户登录成功(验证用户名、密码、验证码),生成登录凭证且设置状态为有效,并将登录凭证存入 Redis 注意:登录凭证存在有效期,在所有的请求执行之前,都会检查凭证是否有效和是否过期,只要该用户的凭证有效并在有效期时间内,本次请求就会一直持有该用户信息(使用 ThreadLocal 持有用户信息)
勾选记住我,则延长登录凭证有效时间
用户登录成功,将用户信息短暂存入 Redis(1 小时)
用户登出,将凭证状态设为无效,并更新 Redis 中该用户的登录凭证信息
将用户选择的头像图片文件上传至七牛云服务器
修改头像
修改密码
未登录用户无法发帖
“版主” 可以看到帖子的置顶和加精按钮并执行相应操作
“管理员” 可以看到帖子的删除按钮并执行相应操作
“普通用户” 无法看到帖子的置顶、加精、删除按钮,也无法执行相应操作
支持按照 “发帖时间” 显示
支持按照 “热度排行” 显示(Spring Quartz)
发布帖子(过滤敏感词),将其存入 MySQL
分页显示所有的帖子
查看帖子详情
权限管理(Spring Security + Thymeleaf Security)
未登录用户无法使用评论功能
发布对帖子的评论(过滤敏感词),将其存入 MySQL
分页显示评论
发布对评论的回复(过滤敏感词)
权限管理(Spring Security)
未登录用户无法使用私信功能
查询某个会话所包含的所有私信
访问私信详情时,将显示的私信设为已读状态
支持分页显示
查询当前用户的会话列表
每个会话只显示一条最新的私信
支持分页显示
发送私信(过滤敏感词)
私信列表
私信详情
权限管理(Spring Security)
普通请求异常
异步请求异常
未登录用户无法使用点赞相关功能
支持对帖子、评论/回复点赞
第 1 次点赞,第 2 次取消点赞
首页统计帖子的点赞数量
详情页统计帖子和评论/回复的点赞数量
详情页显示当前登录用户的点赞状态(赞过了则显示已赞)
统计我的获赞数量
权限管理(Spring Security)
未登录用户无法使用关注相关功能
关注功能
取消关注功能
统计用户的关注数和粉丝数
我的关注列表(查询某个用户关注的人),支持分页
我的粉丝列表(查询某个用户的粉丝),支持分页
权限管理(Spring Security)
未登录用户无法使用系统通知功能
分别显示每种类型的系统通知的未读数量
显示所有系统通知的未读数量
分页显示某一类主题所包含的通知
进入某种类型的系统通知详情,则将该页的所有未读的系统通知状态设置为已读
显示评论、点赞、关注三种类型的通知
通知列表
通知详情
未读数量
导航栏显示所有消息的未读数量(未读私信 + 未读系统通知)
权限管理(Spring Security)
从 Elasticsearch 服务器搜索帖子
从 Elasticsearch 服务器删除帖子(当帖子从数据库中被删除时)
发布帖子时,通过消息队列将帖子异步地提交到 Elasticsearch 服务器
为帖子增加评论时,通过消息队列将帖子异步地提交到 Elasticsearch 服务器
发布事件
搜索服务
显示搜索结果
只有管理员可以查看网站数据统计
存入 Redis 的 Bitmap
支持单日查询和区间日期查询
存入 Redis 的 HyperLogLog
支持单日查询和区间日期查询
独立访客 UV
日活跃用户 DAU
权限管理(Spring Security)
使用本地缓存 Caffeine 缓存热帖列表以及所有用户帖子的总数
各位如果需要将项目部署在本地进行测试,以下环境请提前备好:
Java 8
MySQL 5.7
Redis
Kafka 2.13-2.7.0
Elasticsearch 6.4.3
然后修改配置文件中的信息为你自己的本地环境,直接运行是运行不了的,而且相关私密信息我全部用 xxxxxxx 代替了。
本地运行需要修改的配置文件信息如下:
1)application-develop.properties
:
MySQL
Spring Mail(邮箱需要开启 SMTP 服务)
Kafka:consumer.group-id(该字段见 Kafka 安装包中的 consumer.proerties,可自行修改, 修改完毕后需要重启 Kafka)
Elasticsearch:cluster-name(该字段见 Elasticsearch 安装包中的 elasticsearch.yml,可自行修改)
七牛云(需要新建一个七牛云的对象存储空间,用来存放上传的头像图片)
2)logback-spring-develop.xml
:
LOG_PATH:日志存放的位置
每次运行需要打开:
MySQL
Redis
Elasticsearch
Kafka
另外,还需要事件建好数据库 greatecommunity,然后依次运行项目 sql 文件夹下的这几个 sql 文件建立数据库表:
用户 user
:
DROP TABLE IF EXISTS `user`; SET character_set_client = utf8mb4 ; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, `salt` varchar(50) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `type` int(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;', `status` int(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;', `activation_code` varchar(100) DEFAULT NULL, `header_url` varchar(200) DEFAULT NULL, `create_time` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_username` (`username`(20)), KEY `index_email` (`email`(20)) ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
讨论帖 discuss_post
:
DROP TABLE IF EXISTS `discuss_post`; SET character_set_client = utf8mb4 ; CREATE TABLE `discuss_post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `title` varchar(100) DEFAULT NULL, `content` text, `type` int(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;', `status` int(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;', `create_time` timestamp NULL DEFAULT NULL, `comment_count` int(11) DEFAULT NULL, `score` double DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
评论(回复)comment
:
CREATE TABLE `comment` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `entity_type` int(11) DEFAULT NULL COMMENT '评论目标的类别:1 帖子;2 评论 ', `entity_id` int(11) DEFAULT NULL COMMENT '评论目标的 id', `target_id` int(11) DEFAULT NULL COMMENT '指明对谁进行评论', `content` text, `status` int(11) DEFAULT NULL COMMENT '状态:0 正常;1 禁用', `create_time` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_user_id` (`user_id`), KEY `index_entity_id` (`entity_id`) ) ENGINE=InnoDB AUTO_INCREMENT=247 DEFAULT CHARSET=utf8;
私信 message
:
DROP TABLE IF EXISTS `message`; SET character_set_client = utf8mb4 ; CREATE TABLE `message` ( `id` int(11) NOT NULL AUTO_INCREMENT, `from_id` int(11) DEFAULT NULL, `to_id` int(11) DEFAULT NULL, `conversation_id` varchar(45) NOT NULL, `content` text, `status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;', `create_time` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_from_id` (`from_id`), KEY `index_to_id` (`to_id`), KEY `index_conversation_id` (`conversation_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我每个都只部署了一台,以下是理想的部署架构:
画了一些不是那么严谨的图帮助各位小伙伴理清思绪。
单向绿色箭头:
前端模板 -> Controller:表示这个前端模板中有一个超链接是由这个 Controller 处理的
Controller -> 前端模板:表示这个 Controller 会像该前端模板传递数据或者跳转
双向绿色箭头:表示 Controller 和前端模板之间进行参数的相互传递或使用
单向蓝色箭头: A -> B,表示 A 方法调用了 B 方法
单向红色箭头:数据库或缓存操作
用户注册成功,将用户信息存入 MySQL,但此时该用户状态为未激活
向用户发送激活邮件,用户点击链接则激活账号(Spring Mail)
进入登录界面,动态生成验证码,并将验证码短暂存入 Redis(60 秒)
用户登录成功(验证用户名、密码、验证码),生成登录凭证且设置状态为有效,并将登录凭证存入 Redis
注意:登录凭证存在有效期,在所有的请求执行之前,都会检查凭证是否有效和是否过期,只要该用户的凭证有效并在有效期时间内,本次请求就会一直持有该用户信息(使用 ThreadLocal 持有用户信息)
勾选记住我,则延长登录凭证有效时间
用户登录成功,将用户信息短暂存入 Redis(1 小时)
用户登出,将凭证状态设为无效,并更新 Redis 中该用户的登录凭证信息
下图是登录模块的功能逻辑图,并没有使用 Spring Security 提供的认证逻辑(我觉得这个模块是最复杂的,这张图其实很多细节还没有画全)
支持按照 “发帖时间” 显示
支持按照 “热度排行” 显示(Spring Quartz)
将热帖列表和所有帖子的总数存入本地缓存 Caffeine(利用分布式定时任务 Spring Quartz 每隔一段时间就刷新计算帖子的热度/分数 — 见下文,而 Caffeine 里的数据更新不用我们操心,它天生就会自动的更新它拥有的数据,给它一个初始化方法就完事儿)
修改头像(异步请求)
将用户选择的头像图片文件上传至七牛云服务器
修改密码
此处只画出修改头像:
评论部分前端的名称显示有些缺陷,有兴趣的小伙伴欢迎提 PR 解决~
关于评论模块需要注意的就是评论表的设计,把握其中字段的含义,才能透彻了解这个功能的逻辑。
评论 Comment 的目标类型(帖子,评论) entityType 和 entityId 以及对哪个用户进行评论/回复 targetId 是由前端传递给 DiscussPostController 的
一个帖子的详情页需要封装的信息大概如下:
将点赞相关信息存入 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 就是这个用户的获赞数量
若 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 中,然后通过分布式的定时任务 Spring Quartz,每隔一段时间就从缓存中取出这些帖子进行计算分数。
帖子分数/热度计算公式:分数(热度) = 权重 + 发帖距离天数
// 计算权重 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);
想要自己从零开始实现这个项目或者深入理解的小伙伴,可以关注公众号『飞天小牛肉』,回复 Echo 获取配套教程, 不仅会详细解释本项目涉及的各大技术点,还会汇总相关的常见面试题,目前尚在更新中
并推荐我的开源教程类项目 『 CS-Wiki 』,Gitee 推荐项目,目前已 1.0k+ star: 致力打造完善的 Java 后端知识体系,不仅仅帮助各位小伙伴快速且系统的准备面试(秋招/社招),更指引学习的方向