“心晴疗愈社平台”这个名字听起来就很文艺,实际它是我用 SpringBoot 做的一个心理健康服务类毕设项目。简单说,就是做一个集心理科普文章、情绪日记、咨询师预约、社区互助交流于一体的线上服务平台。项目整体采用 SpringBoot 2.7 + Vue 3 + MyBatis-Plus 前后端分离架构,我把前端打包后也试过塞进 SpringBoot 的静态目录里,一个 Jar 包就能跑起来。这篇文章把整个项目从设计到落地完整复盘一遍,包括数据库表结构、JWT 登录、预约咨询的幂等处理、MinIO 文件上传、定时推送这些核心实现,最后再把我踩过的坑和排查思路整理成一份速查表。适合正在做 SpringBoot 毕设、或者想快速搭建一个带预约+社区功能 Web 平台的同学参考,项目整体难度中等,麻雀虽小但五脏俱全。
1. 项目定调:疗愈社平台到底要解决什么问题
1.1 做这个平台的初衷与功能定位
做系统之前必须先想清楚一个问题:这个平台是给谁用的、解决什么痛点。当前不少人有情绪压力、焦虑、睡眠困扰之类的心理亚健康状态,但直接走进线下咨询室往往有心理门槛,大多数人更愿意先在线上看看文章、写写日记、找人匿名聊聊。所以这个平台的核心定位不是“在线问诊”,而是“轻量级情绪陪伴与心理科普”。
基于这个定位,我把用户端功能拆成四大块:
- 心理资讯:管理员发布心理健康类文章,用户浏览、点赞、评论。
- 情绪日记:用户每天记录情绪状态、事件描述、打分,后台可以做简单的情绪标签提取和趋势图表展示。
- 咨询师预约:咨询师上传简介、维护可预约时间段,用户选择时间完成预约。
- 社区互动:用户在“疗愈广场”发帖、回帖,互相鼓励,营造轻社交氛围。
管理端则负责用户管理、内容审核、咨询师审核、预约记录查看、数据统计。整个系统不碰医疗诊断边界,只做科普和陪伴,这也是我刻意控制的功能范围。
1.2 技术选型背后的取舍逻辑
技术栈选型我考虑过好几套方案,最终定下来的是:
| 技术栈 | 选型 | 理由 |
|---|---|---|
| 核心框架 | SpringBoot 2.7.x | 稳定、生态成熟,云厂商和各类中间件兼容性最好 |
| 持久层 | MyBatis-Plus | 单表 CRUD 几乎不用写 SQL,效率拉满 |
| 数据库 | MySQL 8.x | 免费好用,utf8mb4 支持 emoji |
| 缓存 | Redis | 存验证码、token、点赞计数 |
| 文件存储 | MinIO | 本地部署的 S3 兼容对象存储,做毕设不需要买 OSS |
| 前端 | Vue 3 + Element Plus + ECharts | 组件成熟,图表展示情绪曲线方便 |
| 鉴权 | JWT + 拦截器 | 轻量、代码可控,适合中小型项目 |
为什么不用 Spring Cloud 微服务?这个项目的业务体量一个单体应用完全扛得住,拆微服务只会增加部署和调试成本。SpringBoot 的核心优势就是自动装配,一个spring-boot-starter-web把内嵌 Tomcat、Spring MVC、Jackson 全配好,我只需要关心业务代码。很多人觉得 SpringBoot 是黑盒,其实把spring-boot-autoconfigure里的spring.factories打开看一遍,哪些条件装配生效一目了然,明白了这套机制,后面调 Bug 会快很多。
1.3 项目目录结构与功能清单
项目采用标准的 Maven 多模块单应用结构,包名按功能模块划分:
com.xinqing ├── common // 通用类:Result、异常处理、常量 ├── config // 配置类:RedisConfig、WebMvcConfig、MinIOConfig ├── controller // 控制层 ├── service // 业务层 ├── mapper // 数据层 ├── entity // 实体类 ├── dto // 前端入参对象 ├── vo // 返回视图对象 └── utils // 工具类:JwtUtil、FileUploadUtil、SensitiveWordUtil功能清单整理如下:
- 用户端:手机号+验证码登录、文章列表/详情、文章评论点赞、写情绪日记、情绪趋势图表、咨询师列表/详情、预约咨询师、我的预约、广场发帖/回帖。
- 咨询师端:维护个人简介、维护可预约时段、查看我的预约、填写咨询记录。
- 管理端:用户管理、咨询师审核、文章发布/上下架、评论审核、数据看板。
这套结构做下来,前后端接口一共 60 个左右,单人开发大概 3 周到 4 周能写完,时间大部分花在预约状态流转和权限控制上。
2. 数据库设计与关键表的结构说明
2.1 表结构整体规划
数据库我用的是 MySQL 8.0,字符集统一utf8mb4,排序规则utf8mb4_general_ci。在设计表结构时,我遵循“能拆就拆、不搞大宽表”的原则,一共设计了 12 张核心表:
user 用户表 counselor 咨询师信息表 appointment 预约记录表 schedule 咨询师排班表 mood_diary 情绪日记表 article 文章表 article_comment 文章评论表 like_record 点赞记录表 post 广场帖子表 post_reply 广场回复表 notice 通知表 tag 标签表2.2 用户与咨询师表的细节设计
user 表不做成长表,只放公共属性。业务属性单独放到 counselor 表,通过user_id关联。这比在 user 表里塞角色相关字段干净得多,后续扩展“志愿者”“管理员”等角色不用改主表。
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `phone` varchar(20) NOT NULL , `password` varchar(128) DEFAULT NULL , `nickname` varchar(50) DEFAULT NULL , `avatar` varchar(255) DEFAULT NULL , `gender` tinyint DEFAULT '0', `role` tinyint DEFAULT '1' , `status` tinyint DEFAULT '1' , `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;角色字段我用1表示普通用户,2表示咨询师,3表示管理员。注意普通用户和咨询师不是互斥关系,注册时全是普通用户,申请入驻咨询师并通过审核后,role才改成2,同时插入一条 counselor 记录。
counselor 表字段包括:真实姓名、咨询方向、从业年限、认证信息、个人简介、服务价格、评分、审核状态。审核状态用0待审核 1通过 2拒绝,管理员后台审核通过后,用户端才能看到该咨询师的排班。
2.3 预约与排班表的防冲突设计
预约是这套系统业务最核心的部分,也是最容易出 Bug 的地方。我分了两种表:schedule 存咨询师的可预约时间段,appointment 存用户预约记录。
排班表设计如下:
CREATE TABLE `schedule` ( `id` bigint NOT NULL AUTO_INCREMENT, `counselor_id` bigint NOT NULL, `date` date NOT NULL , `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint DEFAULT '0' , `version` int DEFAULT '0' , PRIMARY KEY (`id`), UNIQUE KEY `uk_counselor_time` (`counselor_id`, `date`, `start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里唯一索引uk_counselor_time直接杜绝了同一个咨询师在同一时间产生两个排班。version字段是留作乐观锁用的,虽然实际预约时我把唯一性锁加在了另一个维度上,但这个字段先留着没坏处。
预约表关键字段是schedule_id、user_id、status。状态流转我用整型枚举:0待开始、1已完成、2已取消、3爽约。用户取消预约时,如果预约还没到开始时间,就释放对应的 schedule 状态,把排班状态从1已预约改回0可约。
2.4 情绪日记表与文章表设计
mood_diary 表我设计得比较灵活,因为情绪本身很难用单一维度描述。除了mood_type存情绪类型外,还加了mood_score1-10 的评分字段,以及content文本内容。写日记的时候用户可以选“开心、平静、焦虑、低落、愤怒、疲惫”等情绪标签,再写一段当时的感受。一周的情绪趋势图其实就是按时间维度聚合mood_score的平均值。
文章表 article 在 content 字段上我用longtext类型,默认值允许为空也不要设。这里有个小坑:如果文章正文要存富文本编辑器生成的 HTML,text类型阈值是 64KB,写长文时容易截断,mediumtext或者longtext更稳妥。
3. 核心功能实现:从登录到预约咨询的完整链路
3.1 JWT 登录与自定义注解权限控制
登录模块我用的是 JWT + 拦截器方案,没用 Spring Security。原因很简单:这个项目需要的只是接口权限校验,不想引入 Security 那一套复杂的过滤链和配置。
用户登录流程:前端输入手机号,后端生成验证码存 Redis,效期 5 分钟。用户输入验证码,后端校验通过后签发 JWT token,返回给前端。token 我放在Authorization请求头里。
JwtUtil 的核心代码:
public class JwtUtil { private static final String SECRET = "xinqing-platform-secret"; private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器每次请求进来先取 header 里的 token,解析成功就把 userId 和 role 放到 ThreadLocal 里,方便 Service 层直接获取当前登录用户。再配合一个自定义注解@RequireRole(role = 2),加在需要咨询师权限的 Controller 方法上,拦截器判断角色不匹配就返回 403。
提示:JWT 的密钥千万不要写死在代码里然后提交到 Git 仓库。我自己的做法是放到
application.yml里,用${JWT_SECRET}环境变量占位,本地开发再给个默认值。虽然毕设项目不会真有人攻击,但养成这个习惯对以后工作有好处。
3.2 预约咨询的幂等与并发控制
预约接口是最容易出并发问题的。用户同时点两次“提交预约”,如果后端不做幂等处理,就会产生两条预约记录,把同一个 schedule 占两次。虽然 schedule 表有唯一索引,但那是约束“排班唯一”,约束不了“预约唯一”。
我的做法是三个层面同时控制:
- 前端提交时带一个
clientToken(UUID),后端 Redis 用SETNX做幂等判断,同一个 clientToken 5 秒内不能重复提交。 - 业务层查询排班时不仅要查 status,还要用乐观锁更新 status,SQL 类似
UPDATE schedule SET status = 1, version = version + 1 WHERE id = ? AND status = 0,影响行数为 0 就说明排班已被抢。 - 数据库给
appointment表加唯一索引uk_schedule_user(schedule_id, user_id),防止同一个用户重复预约同一时段。
三层下来,并发问题基本堵死了。这里想特别强调第二点,很多人做预约系统只查一下 status 再 insert,两个请求同时读到的都是可约状态,然后同时 insert 成功,这就是典型的“检查再执行”竞态。用UPDATE ... WHERE status = 0这种带条件的更新,把“检查”和“更新”合并成一个原子操作,才是正解。
3.3 情绪日记与HanLP关键词提取
情绪日记模块本身 CRUD 没难度,我花时间最多的是情绪标签的自动提取。用户写一段文字“今天被领导骂了一顿,回家路上特别委屈,又累又难过”,如果系统能自动识别“委屈”“难过”这些词并给出情绪倾向,体验会好很多。
这里我接入了 HanLP 分词器。SpringBoot 集成 HanLP 很简单,引入hanlp-portable依赖后直接用HanLP.segment(text)做分词和词性标注。我写了一个简单的情绪词典匹配逻辑:
Map<String, String> moodDict = new HashMap<>(); moodDict.put("开心", "positive"); moodDict.put("高兴", "positive"); moodDict.put("委屈", "negative"); moodDict.put("难过", "negative"); moodDict.put("焦虑", "anxious"); // 省略其他词条 List<String> keywords = HanLP.extractKeyword(content, 5); for (String keyword : keywords) { String moodType = moodDict.get(keyword); if (moodType != null) { moodTags.add(moodType); } }这里要注意,HanLP 默认分词模型对网络用语、口语化表达效果一般,比如“emo”“破防”这种词,标准词典识别不出情绪倾向。我的做法是在情绪词典里手动维护一批网络热词,用用户自定义词典加载。这个词典文件放在 resources 目录下,后续可以随时补充词条,不用改代码。
3.4 社区帖子的敏感内容校验模块
广场发帖是用户生成内容的入口,必须做一层内容安全校验。这里我直接用“分词器 + 自定义敏感词库 + 正则”的组合方式,做了一个轻量校验模块。
敏感词库我维护了一份 txt 文件,每一行一个词。校验时先把帖子内容用 HanLP 分词,再对每个词在敏感词集合里查 HashMap,命中就标记为违规。同时再叠加一层正则表达式,匹配一些特殊变体写法,比如数字中间插空格、同音字替换。命中的帖子不直接删除,而是进入“待审核”状态,管理员后台人工确认后才能展示。
这个方案比调用第三方内容安全 API 可控性好,不用付费、不用网络请求,本地跑就行。缺点是需要自己维护词库,刚开始可以收集项目中使用到的违规词,后续随着用户量上来再逐步补充。
3.5 异步处理与定时任务
系统里有几个场景需要用到异步或定时能力:
- 预约成功通知:预约创建后异步给用户和咨询师各发一条站内信(不是短信,避免付费)。
- 每日心理寄语:每天早上 8 点,用
@Scheduled(cron = "0 0 8 * * ?")从文章表里随机选一条温馨语录推送给所有用户,写入 notice 表。 - 点赞计数同步:用户点赞先写 Redis 的
incrby,然后每 5 分钟跑一次定时任务,把 Redis 里的点赞数刷进 MySQL。
定时任务这里有一个踩坑记录,我最初直接在启动类上加了@EnableScheduling,然后在两个 Service 里写了两个@Scheduled方法。后来发现同一个方法被触发多次,排查半天才发现是多个模块引用了同一个定时任务类,实例化了多份。解决方法是把定时任务单独抽到一个类里,并且只让 Spring 容器管理一份实例。
4. 关键代码实操:SpringBoot配置、文件上传与前端打包部署
4.1 SpringBoot核心配置与多环境切换
application.yml 我拆成了三个环境文件:application-dev.yml、application-prod.yml、application-test.yml,主配置文件里用spring.profiles.active指定当前环境。本地开发用 dev,数据库连接指向 Docker 里的 MySQL,部署到云服务器时用 prod,数据库换云数据库。
开发环境的配置长这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/xinqing?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0有几个配置项特别提醒一下。map-underscore-to-camel-case必须设成 true,不然create_time映射不到createTime属性上。MyBatis-Plus 的逻辑删除配置我一开始没开,后来删评论时发现历史数据全没了,做社区类项目最好都开逻辑删除,不然后面运营数据想查都查不到。
4.2 MyBatis-Plus条件构造器与分页
MyBatis-Plus 让我少写的代码量非常可观。大部分单表查询直接用 LambdaQueryWrapper 就能解决,比如文章列表的分页查询:
public Page<Article> getArticlePage(int pageNum, int pageSize, String keyword) { Page<Article> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Article::getStatus, 1) .and(StringUtils.hasText(keyword), w -> w.like(Article::getTitle, keyword) .or().like(Article::getSummary, keyword)) .orderByDesc(Article::getPublishTime); return articleMapper.selectPage(page, wrapper); }分页插件需要在配置类里注册一个MybatisPlusInterceptor,并添加PaginationInnerInterceptor。我踩过的坑是只加了依赖忘了注册插件,结果分页方法执行后total永远是 0,列表只有当前页数据。这个插件不注册,MyBatis-Plus 就不知道帮你拼LIMIT和COUNT。
4.3 MinIO文件上传与图片回显
图床上传我为什么选 MinIO?因为 OSS 需要实名认证、绑卡、开通服务,做毕设流程麻烦。MinIO 是开源的,一条 Docker 命令就能在本地跑起来,API 和 OSS 兼容,换到生产环境也就是换访问地址和密钥的事。
docker run -d --name minio \ -p 9000:9000 -p 9001:9001 \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ -v /data/minio:/data \ minio/minio server /data --console-address ":9001"SpringBoot 集成 MinIO 只需要引入minioJava SDK,然后写一个配置类:
@Configuration public class MinIOConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Value("${minio.bucket}") private String bucket; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }文件上传接口的核心逻辑就三步:判断桶是否存在(不存在就创建)、用 UUID 重命名文件避免冲突、上传后返回endpoint/bucket/文件名的完整访问地址。注意 MinIO 默认的桶访问权限是私有的,想直接在浏览器打开图片,我是在管理后台把对应 bucket 的访问策略改成readonly,或者配置 Nginx 反向代理/minio路径。
4.4 前端打包与SpringBoot单包部署
前后端分离开发时,前端用 Vite 起的 devServer 通过 proxy 代理转发接口,后端直接跑在 8080 端口,两者不冲突。但部署上线时,如果前端和后端分两个进程跑要维护两套服务,对于小项目来说太繁琐。我把前端打包后的dist静态文件直接放到 SpringBoot 的src/main/resources/static目录下,打成一个 Jar 包部署。
前端要做的适配:
- Vite 配置
base: './',不然打包后资源路径指向根目录,在子路径下全部 404。 - 前端路由用
createWebHashHistory,不要用createWebHistory。因为单页应用在 history 模式下刷新某个子路由请求会走到后端,但是后端没有对应的 mapping,直接 404。 - 后端加一个拦截器,处理页面刷新时的路由回退:对于非
/api开头的 GET 请求,如果请求的是个不存在的路径,直接转发到index.html。
@Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html"); }这行配置把非静态资源的路径全部交给前端路由处理,亲测 Vue Router 的 history 模式刷新也没问题。
4.5 Docker部署SpringBoot项目
部署时我写了一个简单的 Dockerfile,把 Jar 包打进去,然后用 docker-compose 编排 SpringBoot、MySQL、Redis、MinIO 四个容器:
FROM openjdk:11-jre-slim WORKDIR /app COPY target/xinqing-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]docker-compose.yml 里给 SpringBoot 容器配上depends_on依赖 MySQL 和 Redis,环境变量设置SPRING_PROFILES_ACTIVE=prod。有一个坑要重点说:SpringBoot 容器里访问 MySQL 时,localhost指的是容器自己,不是宿主机。必须用 docker-compose 中 MySQL 服务的名称作为主机名,比如jdbc:mysql://mysql:3306/xinqing。我第一次没注意这个问题,容器启动后一直报连接超时,排查了半天才发现是主机写错了。
5. 开发中遇到的常见问题与排查速查表
5.1 SpringBoot版本与依赖兼容问题
开发期间我在 IDEA 里新建项目,SpringBoot 版本默认是 3.x。3.x 要求 Java 17,而我本机 JDK 是 11,启动直接报错。更麻烦的是,网上很多教程都是基于 2.x 的,用 3.x 跑老教程里的spring.factories、WebMvcConfigurer写法会各种不兼容。我直接改成 2.7.18,Java 8 和 Java 11 都能跑,生态兼容性最好。
注意:选 SpringBoot 版本不是越高越好,要看你用的周边组件是否适配。比如 MyBatis-Plus 3.5.x 对 SpringBoot 3 的兼容是单独一个版本,很多 starter 也还没跟上。做毕设求稳,就选 2.7.x。
5.2 LocalDateTime序列化格式问题
后端返回createTime时,前端拿到的默认值是一长串时间戳格式,不是2024-06-01 12:00:00这种易读格式。我一开始在每个字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),加了十几个字段后觉得太蠢了。后来在 application.yml 里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8配置文件解决全局问题,比逐个加注解省事。但要注意,这个配置只对 Java 8 的Date生效,对LocalDateTime需要额外依赖jackson-datatype-jsr310,好在 SpringBoot 默认已经带了。如果还是不生效,检查一下实体类字段是不是LocalDateTime,是的话还得在配置里加一句spring.jackson.serialization.write-dates-as-timestamps: false。
5.3 事务失效与自调用问题
预约流程里我在 Service 中写了几个方法,发现取消预约时明明有@Transactional注解,但数据没有回滚。原因是同一个类里的方法自调用,绕过了 Spring AOP 代理,事务注解不会生效。比如cancelAppointment()调用了同类的releaseSchedule(),releaseSchedule上的@Transactional就直接失效。
解决思路有三种:
- 把需要事务的方法拆到另一个 Service 类里,通过注入调用。
- 在类上使用
@Resource注入自身代理:@Resource private XxxService self;,然后用self.method()调用。 - 直接在当前方法上把所有操作放到一个事务里,写成一个公共入口。
我最终采用第三种,把所有需要回滚的操作合并成一个方法。这样最直观,也不用担心代理问题。
5.4 跨域问题与拦截器优先级
前后端分离开发时,接口跨域是必然碰到的问题。我一开始在 Controller 上加@CrossOrigin,后来发现还是报跨域错误。排查发现拦截器先于跨域处理执行,请求被拦截器拦截后直接返回了 401,浏览器看不到 CORS 响应头,就报跨域错误。
正确做法是全局配置跨域,并且把跨域处理优先级调最高:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }关键点是拦截器里要直接放行OPTIONS预检请求,不然预检请求走到拦截器就被拦住了,后面所有跨域都会以失败告终。
5.5 常见问题速查表
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
启动报Failed to configure a DataSource | 没配数据库连接或配置没生效 | 检查spring.datasource配置,确认 profile 环境是你要的那个 |
| docker 容器连不上 MySQL | 主机名写localhost连的是容器自身 | 改成 docker-compose 里的服务名,如mysql |
| MyBatis-Plus 分页 total 为 0 | 分页插件未注册 | 注入MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| 接口返回 401 或跨域报错 | 拦截器拦截了 OPTIONS 预检请求 | 在拦截器中先判断请求方法,OPTIONS 直接放行 |
| 前端打包后刷新 404 | history 模式路由服务端没配置回退 | 后端转发非/api请求到index.html,或改用 hash 模式 |
| SpringBoot 3 项目启动失败 | JDK 版本不匹配或依赖不兼容 | 降级到 SpringBoot 2.7,保证 JDK 11 或 8 |
@Transactional不生效 | 同类方法自调用绕过代理 | 把事务操作合并到一个入口方法,或注入自身代理 |
这张表基本覆盖了 SpringBoot 项目从 crud 到前后端联调再到部署的主要故障点,很多问题报错信息不明显,核心都是配置或者上下文环境的问题。
6. 个人实操体会与服务扩展方向
6.1 做这套系统给我最大的几个启发
第一,SpringBoot 项目开发效率高,但部署运维才是真正让人头疼的地方。前端打包、Maven 多环境配置、Docker 编排、数据库迁移,每个环节都在考验对整套工具链的理解。我建议刚接触 SpringBoot 的同学不要只盯着写 CRUD,多花时间打通“本地开发 → 打包 → 部署”链路,这个能力在面试中比会写十个增删改查更值钱。
第二,做业务系统要先想清楚状态流转再写代码。预约状态、用户状态、排班状态,这些枚举关系画张图十分钟,但代码一旦写乱,后面改起来要花好几天。状态机里的每个非法跳转(比如“已取消”跳到“已完成”)都应该在 Service 层显式判断并抛异常。
第三,文件存储、缓存、消息推送这类公共能力尽量抽象成工具类,不要散落在业务代码里。我后来把 MinIO 上传封装成了一个StorageService,文章封面、用户头像、帖子图片都用同一个实现,后续要换 OSS 只需要改这一个类。
6.2 如果继续往下扩展,这个平台还能做些什么
做完这套系统后我发现有很多可以进一步延伸的功能。比如接入 WebSocket 做在线即时聊天,让用户和咨询师在预约前先有一段文字沟通;比如用 Redis 的 ZSet 做每周情绪排行榜,增加社区互动玩法;再比如对接微信小程序端,基于现有的 REST API 直接复用。
技术层面的扩展方向更明确:把 JWT 换成 Spring Security + OAuth2 做更细粒度的权限控制,引入消息队列拆分异步通知和日志处理,数据层引入读写分离,这些都可以作为下一阶段的练习方向。
目前这套系统已经完整跑通在 CentOS 服务器上,MySQL、Redis、MinIO、后端 Jar 各司其职,数据也积累了一些真实测试数据。每次打开管理后台看到预约趋势曲线、文章阅读量一点点涨起来,心里还是很有成就感的。做毕设或者练手项目,选一个真实业务场景、用主流技术栈做扎实,比做一堆华而不实的“高并发秒杀系统”要有价值得多。这个经验分享给大家,希望你们的 SpringBoot 项目都能顺利落地。