1. 项目由来与需求定位
1.1 为什么我决定做这样一个项目
后台管理系统写多了,总想搞一个面向真实用户的、能完整跑通的产品。一开始我尝试直接拿开源社区那种大而全的社交平台来二次开发,结果模块太多,部署文档跟不上,光是把 Redis 集群和消息中间件配起来就劝退了我。后来我换了个思路:做一个基于 SpringBoot 的小型社交网络平台,把功能边界控制在一个小范围内,比如注册登录、发动态、关注、点赞、评论,目标用户是校园社团或者小团队内部交流,部署在一台 2 核 4G 的云服务器上就能稳定运行。
这个项目我给它起了个名字叫“简圈”,取“简洁的圈子”的意思。前端采用 Vue 打包后直接放进 SpringBoot 的静态资源目录,最后打成一个可执行的 jar 包,不需要单独搭 Nginx 和 Node 服务,部署成本极低。项目源码、数据库初始化脚本、部署文档我都做了整理,方便后面接手的同学直接跑起来。如果你正准备学习 SpringBoot 全栈开发,或者需要一个能写进简历的小型项目,这篇文章应该对你有帮助。
整个项目从立项到全部功能跑通,大概用了一个月左右的业余时间。最大的感触是:社交类系统虽然听起来高大上,但核心就是围绕“用户关系”和“内容”两张表展开,只要把关系链和内容流设计清楚,剩下的事情都是细节。
1.2 功能需求清单
在动手写代码之前,我先列了一份功能清单,避免做到一半突然想加需求导致设计返工。简圈第一版只做以下核心功能:
- 用户模块:注册、登录、修改个人信息、上传头像。
- 动态模块:发文字动态、图片动态,支持删除自己发布的动态。
- 关系模块:关注其他用户、取消关注,查看我的关注列表和粉丝列表。
- 互动模块:对动态点赞、取消点赞,对动态发表评论,支持一级评论。
- 消息模块:被关注、被点赞、收到评论后产生站内消息,未读消息有红点。
- 信息流:首页展示我关注的人按时间倒序发布的动态,支持分页。
这些功能覆盖了一个社交平台最基本的闭环,但又不会像微博那样复杂到需要专门的内容审核和推荐系统。做这个项目的初衷,是让自己以及后来学习的人能通过一个完整的项目理解 SpringBoot 后端开发的常见要点。
1.3 非功能性需求
除了功能,我还给自己定了几个硬性指标:
- 部署简单:一键打包,依赖尽量只有 JDK、MySQL、Redis 三样。
- 代码可读:包结构清晰,每个 Service 只做自己的事,避免一个类几千行。
- 性能可用:点赞和关注这类高频操作走 Redis,异步落库,避免数据库压力过大。
- 安全基础:密码不能明文存储,登录状态用 JWT 无状态认证,接口需要做登录校验。
2. 技术选型与工程结构设计
2.1 SpringBoot 版本的选择:别盲目追新
技术选型是这类项目最关键的一步,直接影响后面的开发效率。我最初想直接用 SpringBoot 3.x,因为它是最新版本,性能上也有提升。但实际测试发现,SpringBoot 3.x 默认基于 JDK 17,而且把很多javax包迁移到了jakarta,如果团队里其他人还在用 JDK 8,或者后续想部署到一些比较旧的服务器环境,兼容成本会很高。
最后我选择了 SpringBoot 2.7.18,这是 2.x 系列的最后一个版本,稳定且兼容 JDK 8,对 Spring Cloud 生态也支持得很完整。从实际使用体验来看,对于这种小型社交平台,SpringBoot 2.7 和 3.x 的功能差异并不明显,但 2.7 能找到的踩坑资料更多,对新手更友好。这一条经验我会在后文部署部分再详细展开。
2.2 数据访问层:MyBatis-Plus 省去大量重复 SQL
数据访问层我选了 MyBatis-Plus,而不是 Spring Data JPA。原因很简单:社交平台涉及大量连表查询、分页查询、批量更新,MyBatis 的 SQL 掌控力更强。而 MyBatis-Plus 又在原生 MyBatis 基础上提供了 BaseMapper 和通用 Service 接口,像单表 CRUD、分页插件、逻辑删除这些开箱即用,能把开发效率提高一个档次。
举个例子,用户关注的业务需要同时插入关注记录、清理缓存、给对方生成消息,如果用 JPA 的自动建表,表结构设计很容易失控。而 MyBatis-Plus 配合显式的 XML SQL,我可以在 SQL 层面直接把分页、去重、排序做好,后续调优也只需要对着 SQL 分析。
2.3 认证方案:JWT + Redis 而不是 Session
登录状态我选择了 JWT + Redis 的组合。传统 Session 方案在集群部署时需要引入 Session 共享,而 JWT 天然无状态,服务端不需要保存会话记录。但纯 JWT 有个问题:无法主动让某个 token 失效,比如用户修改密码或者管理员封号。所以我引入了 Redis 做 token 黑名单,把登出操作的 token 在过期时间内放入黑名单,达到“无状态 + 可撤回”的效果。
具体流程是:用户登录成功后,后端生成 JWT 并设置过期时间,同时把 token 对应的 userId 存在 Redis 中。每次请求进入拦截器时,先解析 JWT,检验是否在黑名单中,再放行。整套逻辑不复杂,但比裸用 Session 安全得多。
2.4 前端方案:Vue 打包后放进 SpringBoot 静态目录
前端技术栈是 Vue 2 + Element UI,开发完成后执行npm run build,生成一个 dist 目录,里面是纯静态的 HTML、CSS、JS 文件。我把 dist 里的内容全部复制到 SpringBoot 的src/main/resources/static目录下,这样后端服务启动后直接访问http://localhost:8080就能看到首页,不需要额外起 Nginx,也不存在跨域问题。
有人可能会问:为什么不把前后端完全分离部署?如果这个项目后面要扩展到几千人同时在线,那确实应该独立部署前端,方便做 CDN 和负载均衡。但现阶段简圈定位就是轻量、易部署,单体整合是最合理的方案。如果你有前端基础,后续也可以把前端拆出去,SpringBoot 只提供 API,改造起来并不难。
3. 数据库设计与核心表结构
3.1 用户表 user
用户表是系统的根基,字段不需要太多,但每一项都要想清楚。我的设计如下:
id:主键,使用自增 bigint,方便后续分库分表的扩展。username:登录用户名,唯一索引,不允许重复。password:密码用 BCrypt 加密后的字符串,长度设置 60。nickname:昵称,展示用。avatar:头像路径,默认给一个静态路径。bio:个人简介。created_time:注册时间。
这里特别提醒:username和nickname一定要分开。很多新手喜欢用昵称做登录名,一旦允许改昵称,后面的账号体系就乱了。登录名必须稳定且唯一,昵称可以随便改。
3.2 动态表 post
动态表主要负责发帖和内容展示,字段设计要考虑列表页和详情页的查询效率。我的核心字段:
id、user_id:发布者。content:文字内容,类型用 text。images:图片 JSON 字符串,允许多张,例如["/images/1.jpg","/images/2.jpg"]。like_count:点赞数,冗余字段,用于列表直接显示,不需要实时统计。comment_count:评论数,同样冗余。status:状态,0 正常,1 删除。created_time:发布时间。
列表查询按created_time倒序分页,所以要有idx_user_created复合索引,也就是(user_id, created_time)。如果只按 user_id 查某个人的动态,这个索引也能生效。
3.3 关注关系表 follow
关注关系是社交平台的核心,很多新手以为关注就是给用户表加一个 friends 字段,这是完全错误的。我用的是一张独立的关系表:
id主键。user_id:操作人。follow_user_id:被关注的人。created_time。
这里必须建唯一索引(user_id, follow_user_id),防止重复关注。查询某人的粉丝列表时,用follow_user_id作为查询条件,所以还需要一个普通索引idx_follow_user_id。关注数、粉丝数同样可以冗余到 user 表里,但第一版我选择实时统计,因为数据量不大,等用户量上来再通过定时任务或者消息队列异步更新。
3.4 点赞表、评论表与消息表
点赞表post_like设计比较简单:id、user_id、post_id、created_time,同样对(user_id, post_id)建唯一索引,这样就能保证一个人不能对同一动态点赞两次。取消点赞就是删除这条记录。
评论表comment需要支持一级评论,所以包含parent_id,如果为 0 就是顶级评论,否则是某个评论的回复。为了简单,第一版只做展示所有顶级评论,不递归展示回复。
消息表message是最容易被忽略的。我设置了from_user_id、to_user_id、type(1 关注、2 点赞、3 评论)、content、is_read。用户在个人中心看到未读消息数量,就是查询这张表。所有表都使用 InnoDB 引擎,字符串字段默认 utf8mb4,确保能存 emoji。
4. 核心功能的代码实现思路
4.1 统一返回结果与全局异常处理
写接口之前,我先封装了一个Result<T>类,所有接口都用它包装返回,结构统一为code、message、data。这样做的好处是前端处理逻辑不用对乱七八糟的响应格式做兼容。代码很简单,核心就是两个静态方法:Result.success(data)和Result.error(msg)。
异常处理方面,我写了@RestControllerAdvice全局异常拦截器。业务异常使用自定义的BusinessException,统一返回Result.error(e.getMessage()),而系统异常则记录日志后返回“系统繁忙”。这个设计在后端开发里属于基本功,但如果没有一开始规划好,后面接口接近 30 个的时候,再想规范就麻烦了。
4.2 JWT 登录拦截器实现
登录模块我单独讲一下,因为它牵涉到安全。JWT 工具类用io.jsonwebtoken的 jjwt 库,核心方法是生成 token 和解析 token。token 里只放 userId,不放密码等敏感信息。配置一个LoginInterceptor,实现HandlerInterceptor接口,在preHandle里读取请求头Authorization,校验 token 是否有效,如果有效就把 userId 放入ThreadLocal中,后续 Service 层直接从上下文中拿当前用户。
这里有个经验:拦截器里不要把用户信息查出数据库再放进上下文,因为每个请求都查一次数据库,在高并发下是很大浪费。JWT 解析出来的 userId 已经够用,需要用户详情时在业务方法中单独查询,再配合 Redis 缓存。
4.3 发动态与首页信息流
发动态业务相对简单:接收前端传来的 content 和 images,组装成 Post 实体插入数据库,同时更新 Redis 中该用户的动态数量缓存。首页信息流的实现思路是:查询当前用户关注的所有用户 ID,然后通过 MyBatis-Plus 分页查询user_id in (关注列表) AND status=0的动态。
这里我踩过一个性能坑:如果关注列表很长,直接拼IN查询会导致 SQL 很长。好在第一版用户量不大,而且关注列表最多几百人,性能没问题。如果后续数据量增大,可以把关注的人发的动态提前推入一个 Redis 时间线,读取时直接分页拉取,但那样会增加项目复杂度,第一版不推荐。
4.4 点赞与关注的并发一致性处理
点赞和关注是高频且并发敏感的操作,如果直接操作数据库,遇到重复请求很容易产生脏数据。我的做法是:点赞操作先操作 Redis 的 Set 集合,把点赞用户 ID 加入集合中,如果加入成功,说明之前没点过赞,接着把点赞事件写入消息队列或者异步线程池,由异步任务去更新数据库中的post_like表和like_count字段。
关注逻辑类似,先查 Redis 缓存中是否已经有关注关系,再写数据库。虽然异步落库会带来短暂的数据不一致(比如点赞数要过几百毫秒才更新),但对于一个内部交流平台来说完全可接受,而且用户体验上不会有明显感知。如果追求强一致,可以引入分布式事务,但那种复杂度对于这种体量的项目属于过度设计。
5. 源码目录与配置文件精读
5.1 工程目录结构
为了让代码结构一眼能看懂,我按功能分包,而不是按层分包。实际目录如下:
src/main/java/com/simplecircle common // 通用返回体、常量、异常类 config // WebMvc配置、MyBatisPlus分页插件、Redis配置 controller // 接口层 service // 业务层 service/impl // 业务实现 mapper // MyBatis-Plus的Mapper接口 entity // 数据库实体类 dto // 前端请求参数封装 vo // 返回给前端的视图对象 src/main/resources mapper // XML SQL文件 static // 前端构建产物 sql/init.sql // 初始化数据库脚本 application.yml application-prod.yml这种结构的好处是,拿到项目的人可以从 entity 看表结构,从 controller 看 API 入口,从 service 看业务逻辑,层层递进。如果按 controller/service/mapper 水平分包,越来越多的时候反而看不清楚。
5.2 pom.xml 核心依赖
pom.xml 里重点依赖我就列几项:
spring-boot-starter-web:Web 基础。mybatis-plus-boot-starter:版本 3.5.3,注意要和 SpringBoot 2.7 兼容。mysql-connector-j:MySQL 驱动。spring-boot-starter-data-redis:Redis 操作。jjwt-api、jjwt-impl、jjwt-jackson:JWT 相关。hutool-all:工具包,做字符串、日期处理很方便。lombok:减少 getter/setter。
版本号尽量用父工程统一管理,不要到处写死。如果后面升级 SpringBoot,依赖冲突会少很多。
5.3 application.yml 配置细节
本地开发环境和生产环境我用多 profile 区分。下面是基础配置片段:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/simplecircle?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 redis: host: localhost port: 6379 password: database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.simplecircle.entity configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里最容易被忽视的是数据库 URL 里的serverTimezone。如果缺失,连接 MySQL 8 的时候经常会因为时区问题直接报错。这个问题我在部署篇会详细说。
5.4 初始化 SQL 脚本
项目里附带了一个sql/init.sql,里面有建库、建表、插入管理员账号的语句。第一次部署时只需要执行一遍。建议把测试数据也写进去,比如几个模拟用户、几条动态,这样前端开发的时候不至于没有数据展示。
6. 本地与服务器部署全过程
6.1 环境准备清单
部署前要确保本机或者服务器具备以下环境:
- JDK 8 或 JDK 11,注意 SpringBoot 2.7 在 JDK 8 下最稳妥。
- Maven 3.6 以上。
- MySQL 5.7 或 8.0。
- Redis 5.0 以上。
- 服务器为 Linux 系统,推荐 CentOS 7 或 Ubuntu 20.04。
如果你用的是云服务器,记得在安全组和防火墙放行 8080 端口,否则外部访问不到。
6.2 初始化数据库与修改配置
拿到源码后,先创建数据库:
mysql -uroot -p < sql/init.sql然后修改application.yml中的数据库用户名密码、Redis 密码。如果 Redis 没有密码,password留空即可。如果是生产环境,建议不要使用默认密码,至少使用一个强密码,并且把 Redis 绑定到内网 IP 或本机。
6.3 打包运行
在项目根目录执行:
mvn clean package -DskipTests打包完成后,target 目录下会生成simplecircle.jar。然后启动:
java -jar target/simplecircle.jar --spring.profiles.active=prod如果想让项目后台运行,用nohup:
nohup java -jar target/simplecircle.jar --spring.profiles.active=prod > app.log 2>&1 &访问http://服务器IP:8080就能看到前端页面。到这里,一个基于 SpringBoot 的小型社交网络平台就算部署成功了。
6.4 Vue 前端如何整合到 SpringBoot
如果你拿到了前端源码,想要自己改页面再整合进去,步骤如下:先在前端项目根目录执行npm install和npm run build,构建成功后打开 dist 目录,把里面的index.html、css、js、static等文件全部复制到 SpringBoot 的src/main/resources/static目录下。重新执行 Maven 打包,前端就随着 jar 包一起发布了。
这里有个小坑:如果 Vue 路由用了 history 模式,刷新页面会出现 404。最简单的解决办法是把路由改成 hash 模式,URL 里会带个#,对内部系统来说无伤大雅。如果坚持用 history 模式,需要在后端写一个ErrorPage转发到index.html,但这个配置比较繁琐,第一版不建议折腾。
7. 部署过程中几个典型坑与排查链路
7.1 SpringBoot 版本太高引发的 javax/jakarta 报错
我在开发时最开始建了一个 SpringBoot 3.2 的工程,结果本地一编译就报错,很多原先熟悉的类路径变了,比如javax.servlet变成了jakarta.servlet,市面上大部分旧教程都不适用。排查过程是这样的:先看 Maven 依赖树,发现 spring-boot-starter-parent 被解析为 3.2.1,JDK 也提示要求 17。继续看编译错误,问题集中在HttpServletRequest、HttpServletResponse这些类导入失败。
最终我把 pom 里的spring-boot-starter-parent版本改为 2.7.18,同时本地 JDK 切换回 1.8,所有jakarta开头的依赖又重新变回javax,编译错误一扫而空。这个坑提醒大家:如果只是为了做中小企业级的小项目,真没必要追着新版本跑,稳定压倒一切。
7.2 MySQL 时区问题导致 SQL 执行失败
第一次部署到服务器时,SpringBoot 项目能启动起来,但一执行查询就报错:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这个是因为 JDBC 连接 MySQL 8 时,MySQL 驱动要求明确指定时区,而服务器默认时区是 CST,驱动解析不了。
排查链路并不复杂:先查 MySQL 的全局时区,再查 JDBC URL,发现 URL 里缺少serverTimezone参数。加上了serverTimezone=Asia/Shanghai后,问题解决。同时我还顺手加了useSSL=false和allowPublicKeyRetrieval=true,避免本地 SSL 认证失败。如果你一开始就在连接参数中把这些写好,就不会有这个坑。
7.3 Redis 连接拒绝导致登录接口 500
登录接口一开始在前端永远是 500 报错,后端日志显示RedisConnectionFailureException: Unable to connect to Redis。我第一反应是 Redis 没启动,但systemctl status redis显示是 running。进一步排查,发现 Redis 的 bind 配置是127.0.0.1,只允许本机访问,而 SpringBoot 配置的 host 是localhost,理论上没问题。
真正的坑是我在云服务器上部署时,SpringBoot 启动在 Docker 容器里,而 Redis 跑在宿主机上,容器内的localhost指向容器本身,自然连不上宿主机。解决办法是让容器网络使用 host 模式,或者把 host 配置成宿主机的内网 IP。如果你不用 Docker,直接java -jar跑,一般不会遇到这个问题。另外,如果 Redis 开启了保护模式且没有设置密码,外部连接也会被拒绝,需要设置requirepass并在 application.yml 里配置对应的 password。
7.4 前端刷新 404 与静态资源缓存不更新
部署完前端页面后,用户反馈刷新页面偶尔会变成 404。排查后发现,我用的是 Vue Router 的 history 模式,在前端路由跳转正常,但浏览器直接刷新/user/123这个路径时,服务器找不到对应的后端接口,于是返回 404。我后来把路由模式改成了hash,即 URL 变成/#/user/123,刷新后始终跳到index.html,由前端自己解析路由,问题得到解决。
另一个现象是更新前端代码后,重新打 jar 包部署,浏览器打开页面还是旧的 CSS 和 JS。这是浏览器缓存导致的,最简单的做法是在index.html中给静态资源加版本号,比如app.js?v=20250101,或者构建时给文件名加入 hash 值。Vue 默认构建生成的 JS 文件本来就带 hash,如果是自己手写的静态资源,就要注意加版本号。
8. 后续可以做的优化与维护建议
8.1 从单体到微服务的扩展路径
简圈第一版是标准的单体应用,后期如果用户量增长到几千人,依然扛得住。再往上走,我建议先不要急着拆分微服务,而是先把 MySQL 做读写分离、把 Redis 缓存命中率优化上去、把动态流改造为推送模式。如果你实在想拆,可以按用户服务、内容服务、消息服务三个模块拆,但必须先做好接口版本管理和数据库解耦,不然拆了比不拆更痛苦。
8.2 消息推送与即时通讯
目前的站内消息是轮询查询,用户需要刷新才知道有没有新消息。后续可以引入 WebSocket,让用户在线时实时收到点赞和评论推送。WebSocket 的接入并不复杂,SpringBoot 提供了原生支持,关键是要设计好连接管理和消息格式。如果不想为了一个模块引入太多依赖,也可以用定时轮询+Redis 实现一个伪实时效果,至少在功能层面不会有明显缺陷。
8.3 运维与监控日常
项目上线后,我给自己写了几个简单的运维脚本:一个启动脚本、一个停止脚本、一个日志清理脚本。日志文件用logback按天滚动,保留 7 天。监控方面,如果机器资源紧张,可以只保留最核心的三个监控指标:CPU、内存、磁盘空间,用top命令和df -h就够了。真正的 APM 监控可以等系统跑稳定之后再考虑,前期过度建设没有意义。
我个人在实际维护中发现,这种小项目最容易出问题的不是代码逻辑,而是依赖的环境不统一。比如有人用 JDK 8 有人用 JDK 17,有人 MySQL 5.7 有人 MySQL 8,最终表现完全不一样。所以我强烈建议把部署过程写成文档放到项目仓库里,同时录一个视频或者写一篇博客记录踩坑日志,下次部署时照着执行,能省掉大量排查时间。这个就是我从简圈项目里得到的最实在的经验。