📚 項目簡介
Echo 是一套前后端不分離的開源社區系統,基於目前主流 Java Web 技術棧(SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ...),並提供詳細的開發文檔和配套教程。包含帖子、評論、私信、系統通知、點贊、關注、搜索、用戶設置、數據統計等模塊。
源碼鏈接:已托管在 Github 和 Gitee:
在線體驗:項目已經部署到騰訊雲服務器,各位小伙伴們可直接線上體驗: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 中該用戶的登錄憑證信息
-
- 賬號設置
-
修改頭像
-
將用戶選擇的頭像圖片文件上傳至七牛雲服務器
-
-
修改密碼
-
- 帖子模塊
-
發布帖子(過濾敏感詞),將其存入 MySQL
-
分頁顯示所有的帖子
-
支持按照 “發帖時間” 顯示
-
支持按照 “熱度排行” 顯示(Spring Quartz)
-
-
查看帖子詳情
-
權限管理(Spring Security + Thymeleaf Security)
-
未登錄用戶無法發帖
-
“版主” 可以看到帖子的置頂和加精按鈕並執行相應操作
-
“管理員” 可以看到帖子的刪除按鈕並執行相應操作
-
“普通用戶” 無法看到帖子的置頂、加精、刪除按鈕,也無法執行相應操作
-
-
- 評論模塊
-
發布對帖子的評論(過濾敏感詞),將其存入 MySQL
-
分頁顯示評論
-
發布對評論的回復(過濾敏感詞)
-
權限管理(Spring Security)
-
未登錄用戶無法使用評論功能
-
-
- 私信模塊
-
發送私信(過濾敏感詞)
-
私信列表
-
查詢當前用戶的會話列表
-
每個會話只顯示一條最新的私信
-
支持分頁顯示
-
-
私信詳情
-
查詢某個會話所包含的所有私信
-
訪問私信詳情時,將顯示的私信設為已讀狀態
-
支持分頁顯示
-
-
權限管理(Spring Security)
-
未登錄用戶無法使用私信功能
-
-
- 統一處理 404 / 500 異常
-
普通請求異常
-
異步請求異常
-
- 統一記錄日志
- 點贊模塊
-
支持對帖子、評論/回復點贊
-
第 1 次點贊,第 2 次取消點贊
-
首頁統計帖子的點贊數量
-
詳情頁統計帖子和評論/回復的點贊數量
-
詳情頁顯示當前登錄用戶的點贊狀態(贊過了則顯示已贊)
-
統計我的獲贊數量
-
權限管理(Spring Security)
-
未登錄用戶無法使用點贊相關功能
-
-
- 關注模塊
-
關注功能
-
取消關注功能
-
統計用戶的關注數和粉絲數
-
我的關注列表(查詢某個用戶關注的人),支持分頁
-
我的粉絲列表(查詢某個用戶的粉絲),支持分頁
-
權限管理(Spring Security)
-
未登錄用戶無法使用關注相關功能
-
-
- 系統通知模塊
-
通知列表
-
顯示評論、點贊、關注三種類型的通知
-
-
通知詳情
-
分頁顯示某一類主題所包含的通知
-
進入某種類型的系統通知詳情,則將該頁的所有未讀的系統通知狀態設置為已讀
-
-
未讀數量
-
分別顯示每種類型的系統通知的未讀數量
-
顯示所有系統通知的未讀數量
-
-
導航欄顯示所有消息的未讀數量(未讀私信 + 未讀系統通知)
-
權限管理(Spring Security)
-
未登錄用戶無法使用系統通知功能
-
-
- 搜索模塊
-
發布事件
-
發布帖子時,通過消息隊列將帖子異步地提交到 Elasticsearch 服務器
-
為帖子增加評論時,通過消息隊列將帖子異步地提交到 Elasticsearch 服務器
-
-
搜索服務
-
從 Elasticsearch 服務器搜索帖子
-
從 Elasticsearch 服務器刪除帖子(當帖子從數據庫中被刪除時)
-
-
顯示搜索結果
-
- 網站數據統計(管理員專屬)
-
獨立訪客 UV
-
存入 Redis 的 HyperLogLog
-
支持單日查詢和區間日期查詢
-
-
日活躍用戶 DAU
-
存入 Redis 的 Bitmap
-
支持單日查詢和區間日期查詢
-
-
權限管理(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 后端知識體系,不僅僅幫助各位小伙伴快速且系統的准備面試(秋招/社招),更指引學習的方向
