{"id":14983054,"url":"https://github.com/veal98/echo","last_synced_at":"2025-04-12T22:18:44.620Z","repository":{"id":38244754,"uuid":"330304527","full_name":"Veal98/Echo","owner":"Veal98","description":"🦄 论坛：基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。包含帖子、评论、私信、系统通知、点赞、关注、搜索、用户设置、数据统计等模块。","archived":false,"fork":false,"pushed_at":"2024-05-15T11:53:12.000Z","size":25847,"stargazers_count":710,"open_issues_count":5,"forks_count":156,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-12T22:17:50.188Z","etag":null,"topics":["caffeine","elasticsearch","java","kafka","mybatis","mysql","redis","spring","springboot","springmvc","springsecurity"],"latest_commit_sha":null,"homepage":"https://itmtx.cn/column/12","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Veal98.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-01-17T03:12:42.000Z","updated_at":"2025-04-02T01:50:34.000Z","dependencies_parsed_at":"2024-09-29T06:16:08.978Z","dependency_job_id":null,"html_url":"https://github.com/Veal98/Echo","commit_stats":{"total_commits":66,"total_committers":4,"mean_commits":16.5,"dds":"0.10606060606060608","last_synced_commit":"1175a46dd6ac8246acb180f2bbdf3ca1781c179f"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Veal98%2FEcho","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Veal98%2FEcho/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Veal98%2FEcho/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Veal98%2FEcho/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Veal98","download_url":"https://codeload.github.com/Veal98/Echo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248637946,"owners_count":21137577,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["caffeine","elasticsearch","java","kafka","mybatis","mysql","redis","spring","springboot","springmvc","springsecurity"],"created_at":"2024-09-24T14:06:39.808Z","updated_at":"2025-04-12T22:18:44.586Z","avatar_url":"https://github.com/Veal98.png","language":"JavaScript","readme":"# Echo — 开源社区系统\n\n---\n\n\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg width=\"\" src=\"https://gitee.com/veal98/images/raw/master/img/20210211175136.png\" \u003e\n\u003c/p\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n\n[![star](https://gitee.com/veal98/Echo/badge/star.svg?theme=dark)](https://gitee.com/veal98/Echo/stargazers)\n[![fork](https://gitee.com/veal98/Echo/badge/fork.svg?theme=dark)](https://gitee.com/veal98/Echo/members)\n[![GitHub stars](https://img.shields.io/github/stars/Veal98/Echo?logo=github)](https://github.com/Veal98/Echo/stargazers)\n[![GitHub forks](https://img.shields.io/github/forks/Veal98/Echo?logo=github)](https://github.com/Veal98/Echo/network)\n[![version](https://img.shields.io/badge/version-2.1-brightgreen)]()\n\n\n\u003c/div\u003e\n\n\u003e 各位可以参考配套教程自己拉下去部署\n\n## 📚 项目简介\n\nEcho 是一套前后端不分离的开源社区系统，基于目前主流 Java Web 技术栈（SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ...），并提供详细的开发文档和配套教程。包含帖子、评论、私信、系统通知、点赞、关注、搜索、用户设置、数据统计等模块。\n\n**源码链接**：已托管在 Github 和 Gitee：\n\n- Gitee：[https://gitee.com/veal98/Echo](https://gitee.com/veal98/Echo)（Gitee 官方推荐项目）\n- Github：[https://github.com/Veal98/Echo](https://github.com/Veal98/Echo)\n\n**教程地址**：文档通过 \u003cu\u003eDocsify + Github/Gitee Pages\u003c/u\u003e 生成\n\n- [https://itmtx.cn/column/12](https://itmtx.cn/column/12)\n\n\n## 💻 核心技术栈\n\n后端：\n\n- Spring\n- Spring Boot 2.1.5 RELEASE\n- Spring MVC\n- ORM：MyBatis\n- 数据库：MySQL 5.7\n- 分布式缓存：Redis\n- 本地缓存：Caffeine\n- 消息队列：Kafka 2.13-2.7.0\n- 搜索引擎：Elasticsearch 6.4.3\n- 安全：Spring Security\n- 邮件任务：Spring Mail\n- 分布式定时任务：Spring Quartz\n- 日志：SLF4J（日志接口） + Logback（日志实现）\n\n前端：\n\n- Thymeleaf\n- Bootstrap 4.x\n- Jquery\n- Ajax\n\n## 🔨 开发环境\n\n- 操作系统：Windows 10\n- 构建工具：Apache Maven\n- 集成开发工具：Intellij IDEA\n- 应用服务器：Apache Tomcat\n- 接口测试工具：Postman\n- 压力测试工具：Apache JMeter\n- 版本控制工具：Git\n- Java 版本：8\n\n## 🎀 界面展示\n\n首页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205641.png)\n\n登录页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205558.png)\n\n密码重置页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210709003428.png)\n\n帖子发布页（支持 MarkDown）：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210512101041.png)\n\n帖子详情页（MarkDown 渲染）：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210512101202.png)\n\n帖子详情页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205741.png)\n\n\n\n个人主页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205820.png)\n\n朋友私信页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205857.png)\n\n私信详情页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211205948.png)\n\n系统通知页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211210122.png)\n\n通知详情页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211210152.png)\n\n账号设置页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211210238.png)\n\n数据统计页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211210323.png)\n\n搜索详情页：\n\n![](https://gitee.com/veal98/images/raw/master/img/20210211210531.png)\n\n## 🎨 功能列表\n\n![](https://gitee.com/veal98/images/raw/master/img/20210208222403.png)\n\n- [x] **注册**\n- [x] **登录 | 登出**\n  - 动态生成验证码\n  - 记住我\n- [x] **账号设置**\n  - 修改头像\n  - 修改密码\n- [x] **过滤敏感词**\n  - 前缀树\n- [x] **帖子模块**\n  - 发布帖子（过滤敏感词）\n  - 分页显示所有的帖子\n    - 支持按照 “发帖时间” 显示\n    - 支持按照 “热度排行” 显示（Spring Quartz）\n  - 查看帖子详情\n  - 权限管理（Spring Security + Thymeleaf Security）\n    - 未登录用户无法发帖\n    - “版主” 可以看到帖子的置顶和加精按钮并执行相应操作\n    - “管理员” 可以看到帖子的删除按钮并执行相应操作\n    - “普通用户” 无法看到帖子的置顶、加精、删除按钮，也无法执行相应操作\n- [x] **评论模块**\n  - 发布对帖子的评论（过滤敏感词）\n  - 分页显示评论\n  - 发布对评论的回复（过滤敏感词）\n  - 权限管理（Spring Security）\n    - 未登录用户无法使用评论功能\n- [x] **私信模块**\n  - 发送私信（过滤敏感词）\n  - 私信列表\n    - 查询当前用户的会话列表\n    - 每个会话只显示一条最新的私信\n    - 支持分页显示\n  - 私信详情\n    - 查询某个会话所包含的所有私信\n    - 访问私信详情时，将显示的私信设为已读状态\n    - 支持分页显示\n  - 权限管理（Spring Security）\n    - 未登录用户无法使用私信功能\n- [x] **统一处理 404 / 500 异常**\n  - 普通请求异常\n  - 异步请求异常\n- [x] **统一记录日志**\n- [x] **点赞模块**\n  - 支持对帖子、评论/回复点赞\n  - 第 1 次点赞，第 2 次取消点赞\n  - 首页统计帖子的点赞数量\n  - 详情页统计帖子和评论/回复的点赞数量\n  - 详情页显示当前登录用户的点赞状态（赞过了则显示已赞）\n  - 统计我的获赞数量\n  - 权限管理（Spring Security）\n    - 未登录用户无法使用点赞相关功能\n- [x] **关注模块**\n  - 关注功能\n  - 取消关注功能\n  - 统计用户的关注数和粉丝数\n  - 我的关注列表（查询某个用户关注的人），支持分页\n  - 我的粉丝列表（查询某个用户的粉丝），支持分页\n  - 权限管理（Spring Security）\n    - 未登录用户无法使用关注相关功能\n- [x] **系统通知模块**\n  - 通知列表\n    - 显示评论、点赞、关注三种类型的通知\n  - 通知详情\n    - 分页显示某一类主题所包含的通知\n    - 进入某种类型的系统通知详情，则将该页的所有未读的系统通知状态设置为已读\n  - 未读数量\n    - 分别显示每种类型的系统通知的未读数量\n    - 显示所有系统通知的未读数量\n  - 导航栏显示所有消息的未读数量（未读私信 + 未读系统通知）\n  - 权限管理（Spring Security）\n    - 未登录用户无法使用系统通知功能\n- [x] **搜索模块**\n- [x] **网站数据统计**（管理员专属）\n  - 独立访客 UV\n    - 支持单日查询和区间日期查询\n  - 日活跃用户 DAU\n    - 支持单日查询和区间日期查询\n  - 权限管理（Spring Security）\n    - 只有管理员可以查看网站数据统计\n- [x] **优化网站性能**\n  - 处理每次请求时，都要通过拦截器根据登录凭证查询用户信息，访问的频率非常高。因此将已成功登录的用户信息在缓存 Redis 中保存一段时间，查询用户信息的时候优先从缓存中取值；若缓存中没有该用户信息，则将其存入缓存；用户信息变更时清除对应的缓存数据\n  - 引入本地缓存 Caffeine，缓存热帖列表和帖子的总数，提升响应速度（可以进一步采用二级缓存架构）\n\n## 🌱 本地运行\n\n各位如果需要将项目部署在本地进行测试，以下环境请提前备好：\n\n- Java 8\n- MySQL 5.7\n- Redis\n- Kafka 2.13-2.7.0\n- Elasticsearch 6.4.3\n\n然后**修改配置文件中的信息为你自己的本地环境，直接运行是运行不了的**，而且相关私密信息我全部用 xxxxxxx 代替了。\n\n本地运行需要修改的配置文件信息如下：\n\n1）`application-develop.properties`：\n\n- MySQL\n- Spring Mail（邮箱需要开启 SMTP 服务）\n- Kafka：consumer.group-id（该字段见 Kafka 安装包中的 consumer.proerties，可自行修改, 修改完毕后需要重启 Kafka）\n- Elasticsearch：cluster-name（该字段见 Elasticsearch 安装包中的 elasticsearch.yml，可自行修改）\n- 七牛云（需要新建一个七牛云的对象存储空间，用来存放上传的头像图片）\n\n2）`logback-spring-develop.xml`：\n\n- LOG_PATH：日志存放的位置\n\n每次运行需要打开：\n\n- MySQL\n- Redis\n- Elasticsearch\n- Kafka\n\n另外，还需要事件建好数据库 greatecommunity，然后依次运行项目 sql 文件夹下的这几个 sql 文件建立数据库表：\n\n\u003cimg src=\"https://gitee.com/veal98/images/raw/master/img/20210217134928.png\" style=\"width:386px\" /\u003e\n\n## 🌌 部署架构\n\n我每个都只部署了一台，以下是理想的部署架构：\n\n\u003cimg src=\"https://gitee.com/veal98/images/raw/master/img/20210211204207.png\"  /\u003e\n\n## 🎯 功能逻辑图\n\n画了一些不是那么严谨的图帮助各位小伙伴理清思绪。\n\n\u003e 单向绿色箭头：\n\u003e\n\u003e - 前端模板 -\u003e Controller：表示这个前端模板中有一个超链接是由这个 Controller 处理的\n\u003e - Controller -\u003e 前端模板：表示这个 Controller 会像该前端模板传递数据或者跳转\n\u003e\n\u003e 双向绿色箭头：表示 Controller 和前端模板之间进行参数的相互传递或使用\n\u003e\n\u003e 单向蓝色箭头： A -\u003e B，表示 A 方法调用了 B 方法\n\u003e\n\u003e 单向红色箭头：数据库或缓存操作\n\n### 注册\n\n- 用户注册成功，将用户信息存入 MySQL，但此时该用户状态为未激活\n- 向用户发送激活邮件，用户点击链接则激活账号（Spring Mail）\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210204222249.png\" /\u003e\n\n### 登录 | 登出\n\n登录认证模块跳过了 Spring Secuity 自带的认证机制。主要逻辑如下：\n\n- 进入登录界面，随机生成一个字符串来标识这个将要登录的用户，将这个字符串短暂的存入 Cookie（60 秒）；\n- 动态生成验证码，并将验证码及标识该用户的字符串短暂存入 Redis（60 秒）；\n- 为登录成功（验证用户名、密码、验证码）的用户随机生成登录凭证且设置状态为有效，并将登录凭证及其状态等信息永久存入 Redis，再在 Cookie 中存一份登录凭证；\n- 使用拦截器在所有的请求执行之前，从 Cookie 中获取登录凭证，只要 Redis 中该凭证有效并在有效期内，本次请求就会一直持有该用户信息（使用 ThreadLocal 持有用户信息，保证多台服务器上用户的登录状态同步）；\n- 勾选记住我，则延长 Cookie 中登录凭证的有效时间；\n- 用户登出，将凭证状态设为无效，并更新 Redis 中该登录凭证的相关信息。\n\n下图是登录模块的功能逻辑图，并没有使用 Spring Security 提供的认证逻辑（我觉得这个模块是最复杂的，这张图其实很多细节还没有画全）\n\n![](https://gitee.com/veal98/images/raw/master/img/20210204233233.png)\n\n### 分页显示所有的帖子\n\n- 支持按照 “发帖时间” 显示\n- 支持按照 “热度排行” 显示（Spring Quartz）\n- 将热帖列表和所有帖子的总数存入本地缓存 Caffeine（利用分布式定时任务 Spring Quartz 每隔一段时间就刷新计算帖子的热度/分数 — 见下文，而 Caffeine 里的数据更新不用我们操心，它天生就会自动的更新它拥有的数据，给它一个初始化方法就完事儿）\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210204222822.png\" /\u003e\n\n\n\n### 账号设置\n\n- 修改头像（异步请求）\n  - 将用户选择的头像图片文件上传至七牛云服务器\n- 修改密码\n\n此处只画出修改头像：\n\n\u003cimg width=\"700px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210206121201.png\" /\u003e\n\n### 发布帖子（异步请求）\n\n发布帖子（过滤敏感词），将其存入 MySQL\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210206122521.png\" /\u003e\n\n### 显示评论及相关信息\n\n\u003e 评论部分前端的名称显示有些缺陷，有兴趣的小伙伴欢迎提 PR 解决 ~\n\n关于评论模块需要注意的就是评论表的设计，把握其中字段的含义，才能透彻了解这个功能的逻辑。\n\n评论 Comment 的目标类型（帖子，评论） entityType 和 entityId 以及对哪个用户进行评论/回复 targetId 是由前端传递给 DiscussPostController 的\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207150925.png\" /\u003e\n\n一个帖子的详情页需要封装的信息大概如下：\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207151328.png\" /\u003e\n\n### 添加评论（事务管理）\n\n发布对帖子的评论（过滤敏感词），将其存入 MySQL\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207122908.png\" /\u003e\n\n### 私信列表和详情页\n\n\u003cimg width=\"700px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207161130.png\" /\u003e\n\n### 发送私信（异步请求）\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207161500.png\" /\u003e\n\n### 点赞（异步请求）\n\n将点赞相关信息存入 Redis 的数据结构 set 中。其中，key 命名为  `like:entity:entityType:entityId`，value 即点赞用户的 id。比如 key =  `like:entity:2:246`  value =  `11` 表示用户 11 对实体类型 2 即评论进行了点赞，该评论的 id 是 246\n\n某个用户的获赞数量对应的存储在 Redis 中的 key 是 `like:user:userId`，value 就是这个用户的获赞数量\n\n\u003cimg width=\"700px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207165837.png\"  /\u003e\n\n### 我的获赞数量\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207170003.png\" /\u003e\n\n### 关注（异步请求）\n\n- 若 A 关注了 B，则 A 是 B 的粉丝 Follower，B 是 A 的目标 Followee\n- 关注的目标可以是用户、帖子、题目等，在实现时将这些目标抽象为实体（目前只做了关注用户）\n\n将某个用户关注的实体相关信息存储在 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\n\n同样的，将某个实体拥有的粉丝相关信息也存储在 Redis 的数据结构 zset 中：key 是 `follower:entityType:entityId`，对应的 value 是 `zset(userId, now)`，以关注的时间进行排序\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207174046.png\" /\u003e\n\n### 关注列表\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210207175621.png\" /\u003e\n\n### 发送系统通知\n\n![](https://gitee.com/veal98/images/raw/master/img/20210207182917.png)\n\n### 显示系统通知\n\n![](https://gitee.com/veal98/images/raw/master/img/20210208153059.png)\n\n### 搜索\n\n- 发布事件\n  - 发布帖子时，通过消息队列将帖子异步地提交到 Elasticsearch 服务器\n  - 为帖子增加评论时，通过消息队列将帖子异步地提交到 Elasticsearch 服务器\n- 搜索服务\n  - 从 Elasticsearch 服务器搜索帖子\n  - 从 Elasticsearch 服务器删除帖子（当帖子从数据库中被删除时）\n- 显示搜索结果\n\n![](https://gitee.com/veal98/images/raw/master/img/20210208161936.png)\n\n类似的，置顶、加精也会触发发帖事件，就不再图里面画出来了。\n\n### 置顶加精删除（异步请求）\n\n\u003cimg width=\"660px\" src=\"https://gitee.com/veal98/images/raw/master/img/20210208171729.png\" /\u003e\n\n### 网站数据统计\n\n- 独立访客 UV\n  - 存入 Redis 的 HyperLogLog\n  - 支持单日查询和区间日期查询\n- 日活跃用户 DAU\n  - 存入 Redis 的 Bitmap\n  - 支持单日查询和区间日期查询\n- 权限管理（Spring Security）\n  - 只有管理员可以查看网站数据统计\n\n![](https://gitee.com/veal98/images/raw/master/img/20210208170801.png)\n\n### 帖子热度计算\n\n每次发生点赞（给帖子点赞）、评论（给帖子评论）、加精的时候，就将这些帖子信息存入缓存 Redis 中，然后通过分布式的定时任务 Spring Quartz，每隔一段时间就从缓存中取出这些帖子进行计算分数。\n\n帖子分数/热度计算公式：分数（热度） = 权重 + 发帖距离天数\n\n```java\n// 计算权重\ndouble w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;\n// 分数 = 权重 + 发帖距离天数\ndouble score = Math.log10(Math.max(w, 1))\n        + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);\n```\n\n![](https://gitee.com/veal98/images/raw/master/img/20210208173636.png)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveal98%2Fecho","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fveal98%2Fecho","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveal98%2Fecho/lists"}