每年这个时候,总有不少同学在后台私信问我:SpringBoot 的毕设项目到底该怎么做才能拿得出手?恰好我手头刚完成了一个家政保洁预约系统的完整开发,项目代号就叫mwrnnvi8_zl032,从数据库设计到权限控制再到前后端联调,踩了不少坑,也沉淀了不少可以直接复用的经验。这次就把整个项目从零到一的完整思路拆给你看,不管是拿来当毕设、课程设计,还是自己练手做全栈,这篇都够用了。
1. 项目整体设计与技术选型
1.1 这个项目到底在做什么
家政保洁预约系统,本质上解决的是一个典型的“服务交易撮合”问题:顾客需要保洁服务,保洁员/服务商有供给能力,系统要做的就是把预约、派单、支付、评价这条链路串起来。
从需求视角拆一下,这个系统至少包含三类角色:
- 用户端:注册登录、浏览服务项目、选择时间段、下单预约、在线支付(或下单后线下结算)、查看订单状态、取消订单、评价服务。
- 管理员端:服务项目管理、保洁员管理、订单管理(派单/改派/取消)、用户管理、数据统计、反馈处理。
- 保洁员端:接单提醒、确认接单、完成服务、查看收益记录(如果做到财务模块的话)。
我实际做的时候,把重心放在了用户端和管理员端,保洁员端用的是一个简化版的 H5 页面,逻辑和后台管理端有部分复用。这样做的好处是工作量可控,同时三个端都有了,答辩时讲演示也不虚。
1.2 技术选型:为什么是 SpringBoot 这套组合
先说结论,这套技术栈是这个项目最稳妥的组合,没有之一:
- 后端:Spring Boot 2.7.x + MyBatis-Plus + Spring Security + JWT
- 前端:Vue 2 + Element UI(后台管理端),用户端用的则是轻量的 Bootstrap 模板改造
- 数据库:MySQL 8.0 + Redis(会话/缓存)
- 开发工具:IDEA + Navicat + Postman + Git
- 构建:Maven,打包方式为 Jar,部署到服务器跑 Nginx 反向代理
选 SpringBoot 的核心原因,一是它足够成熟,生态完善,面试时也好讲;二是它对毕设和中小型项目来说开发效率极高,起步快、调试方便,不用像 SSM 那样配一堆 XML。
注意:Spring Boot 3.x 已经出来了,但很多第三方组件(尤其是一些老牌的分页插件、代码生成器)对它的兼容还不太稳定。我这个项目用的是2.7.x 版本,JDK 用 1.8(或者 11 也行)。如果你不是想尝鲜,别在版本上给自己挖坑。
1.3 架构思路:前后端分离但还是“传统”了点
这个项目采用了前后端分离结构,RESTful API 风格。前端通过 Axios 调用后端接口,后端统一返回Result<T>结构。
{ "code": 200, "message": "success", "data": { } }后端内部按四层走:Controller → Service → Mapper → Database,这不是什么花哨的微服务架构,但足够清晰,也好扩展。我一直觉得,做毕设或者中小型真实项目,没必要一上来就拆微服务,单体+模块化就已经是合理上限了。
有一个容易被人忽略的设计点:业务层面做了模块切分。用户模块、订单模块、服务项目模块、评价模块、统计模块之间尽量解耦,加上头条搜索词里有“springboot项目全局过滤器处理上传pdf文件时xss攻击”这种问题,我当时也把全局异常处理和 XSS 过滤统一做了,后面会讲。
2. 数据库设计与表结构核心逻辑
2.1 建表思路:六张核心表
家政预约系统的表设计并不复杂,但如果直接照着业务表一层层堆,后期往往会因为字段冗余反反复复改。我最后沉淀下来的核心表如下:
user:用户表(含角色字段:USER / ADMIN / WORKER)service_category:服务分类表(日常保洁、深度保洁、开荒保洁、家电清洗等)service_item:服务项目表(挂在分类下面,含价格、时长、图片等)appointment_order:预约订单表(核心表)order_status_log:订单状态变更日志表(这个表容易被忽略,但做订单管理必备)review:服务评价表
如果你想扩展功能(比如积分、优惠券、财务结算),在这个基础上加表就可以了,不会伤筋动骨。
2.2 预约订单表的设计细节
订单表是整个系统的“中枢”,我列出几个关键字段,方便大家直接参考:
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_no | varchar | 订单编号,生成规则建议“日期+随机串” |
| user_id | bigint | 下单用户 |
| worker_id | bigint | 派单保洁员 |
| service_item_id | bigint | 服务项目 |
| service_date | date | 预约日期 |
| service_timeslot | varchar | 预约时段,如 09:00-12:00 |
| address | varchar | 服务地址 |
| status | tinyint | 状态机 |
| payment_status | tinyint | 支付状态 |
| total_amount | decimal | 订单金额 |
| remark | varchar | 备注 |
状态机设计我建议这样定义:1-待支付 / 2-待派单 / 3-待服务 / 4-服务中 / 5-已完成 / 6-已取消。这里有个小细节:很多人喜欢只在订单表里保留一个 status 字段,然后直接 update,但这样对“订单被谁在什么时候改过状态”没有记录。我加了order_status_log,虽然多写几行代码,但调试和答辩时真的能加分。
2.3 数据库索引与性能上的几个取舍
用户下单查询、管理员后台订单列表,最常见的查询条件是status、user_id、service_date,所以强烈建议在订单表上建联合索引。我当时建的是:
ALTER TABLE appointment_order ADD INDEX idx_user_status_date (user_id, status, service_date);索引不是越多越好,尤其是在 MySQL 8.0 里。我当时在remark字段上尝试加过全文索引,发现对这种小型系统完全用不上,反而拖慢写入,后来直接删了。如果你的查询条件相对固定,就只建那两三个最有价值的联合索引即可。
3. 核心功能模块与接口设计
3.1 登录鉴权:JWT + Spring Security 还是“轻量拦截器”?
很多同学一看到“Spring Security”就头大,觉得配置太麻烦。我对这类需求的建议是:单纯做毕设,JWT + 拦截器就可以了;如果你希望简历上写 Spring Security,就用它来做。
这个项目我是用 Spring Security + JWT 实现的。核心思路很简单:用户登录成功后,后端生成一个 token 返回给前端,前端存在 localStorage,每次请求时放到请求头Authorization里。后端用一个过滤器校验 token 合法性,并解析出用户信息放到 SecurityContext 里。
关键配置有三块:
WebSecurityConfigurerAdapter配置放行规则(登录、注册、服务列表等接口放行,其他接口需要认证)。JwtAuthenticationFilter:继承OncePerRequestFilter,在请求进入 Controller 前完成 token 校验和用户身份组装。- 密码加密:必须用 BCrypt,不要用 MD5,这一点面试时问到加密时很容易成为加分项。
这里踩过一个大坑:Spring Security 的过滤器链和 CORS 配置之间偶尔会“打架”,如果你遇到“前端请求能到后端但拿不到响应头”的情况,检查一下是不是
corsConfigurationSource没有注册到 Security 的配置里。
3.2 下单预约的核心流程
预约下单的流程比较常规,但有两个细节要额外注意:
第一是库存/名额校验。家政项目的“库存”不是具体商品数量,而是保洁员在某时段的可服务名额。我当时的做法是:在service_item表里增加一个max_orders_per_slot字段,下单时用 Redis 的incr记录某保洁员在某时段的订单数,超过上限就拒绝下单。虽然 MySQL 也能做,但用 Redis 的原子操作更稳,并发场景下不容易超卖。
第二是订单号生成。直接用数据库自增 id 当订单号,很容易暴露业务量,而且不太好看。我用的规则是:
String orderNo = "JZ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + RandomStringUtils.randomNumeric(4);大概就是你看到的JZ202409151430552847这种结构。后来发现并发高的时候同秒可能出现重复随机串,所以加了个“订单号唯一索引+重试机制”,稳妥一点。
3.3 派单逻辑:先用人工派单,再谈自动派单
很多同学在这上面纠结很久,到底要不要做自动派单算法?我的建议是:不要一开始就做复杂的智能派单,那是给自己挖坑。
这个项目采用“管理员派单为主,用户选保洁员为辅”的方式。用户下单时可以选择“指定保洁员”,也可以选择“平台指派”。平台指派模式下,订单进入“待派单”状态,管理员在后台可以查看当前所有订单,手动选择保洁员并点击“派单”。
为什么不做自动派单?因为自动派单需要维护位置距离、实时档期、历史评分、技能匹配等一系列数据,逻辑写起来不算难,但调试真的很耗时。等基础版本稳定之后,我再规划了一个简单的智能派单(按“当日订单数最少 + 评分最高”优先),作为扩展功能。
4. 安全设计:XSS 过滤、统一异常与文件上传
4.1 全局 XSS 过滤器,细节必须抠
搜索词里提到“springboot项目全局过滤器处理上传pdf文件时xss攻击”,这个问题在真实开发里确实会遇到——很多人在文本输入框做了 XSS 过滤,但上传的文件名、PDF 文件的元数据里照样可以塞恶意脚本。
我当时的做法是写了一个XssFilter,继承OncePerRequestFilter,对请求体里的参数做转义处理,重写了HttpServletRequestWrapper的getParameter()、getParameterValues()和getInputStream()。
同时,针对 PDF 上传场景,我在文件上传接口中专门处理了文件名:
String safeFileName = HtmlUtils.htmlEscape(file.getOriginalFilename(), "UTF-8");如果文件类型是 PDF,还会用 PDFBox 解析元数据,把标题、作者等属性里的<script>等标签剔除掉。这一层保护在日常demo里很少见,但真正上线或者参加比赛时会很加分。
4.2 统一异常处理
另一个项目必备项是全局异常处理器,我用@RestControllerAdvice实现了三种异常的分层处理:
- 业务异常(例如余额不足、库存不足),返回 code = 500 + 自定义 message。
- 参数校验异常,返回 code = 400 + 定位到具体字段名。
- 兜底异常,返回 code = 500 + 不包含敏感信息的通用提示。
有一点需要特别提醒:线上环境不要把异常堆栈直接返回给前端,这不仅暴露内部实现细节,而且对用户体验也很差。我当时写了一个逻辑,如果当前是 dev 环境,才在响应里附带堆栈信息;如果是 prod,只在服务端日志里输出。
4.3 文件上传:本地存储还是 MinIO?
这个项目的服务图片、用户头像,最初是存到本地目录的,但随着项目代码在不同电脑之间迁移,路径问题开始频繁冒出来。后来我换成了 MinIO 做对象存储,客户端通过预签名 URL 直传,不经过后端转存。
这样做的好处是:后端不扛流量、不占内存,文件访问走 MinIO 的地址即可,扩展起来也方便。但是有一个细节要特别注意:预签名 URL 是有过期时间的,如果前端页面停留时间过长,图片链接会失效,需要在响应时设置一个足够长的有效期(例如 7 天),或者前端定期刷新 URL。
String presignedUrl = minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .bucket("home-service") .object(objectName) .expiry(60 * 60 * 24 * 7) .build());不过这里也要提醒一下,本地存储对于毕设来说完全够用,MinIO 是为了练手对象存储而加的。如果你时间紧、追求稳定,本地存储或七牛云/阿里云 OSS 都可以。
5. 前后端联调与典型问题排查
5.1 联调之前,先把接口文档定好
很多同学习惯先写完后端再写前端,结果接口字段对不上,联调阶段一地鸡毛。我这次用 Apifox 统一管理接口文档,后端的实体字段和前端 VO 字段保持一致,再生成前端模拟数据。这样一来,前端和后端完全可以并行开发。
这里有一个实用建议:接口返回的数据结构,不要直接返回实体类(Entity),而是返回 VO(View Object)。因为实体类往往会包含密码、手机号、逻辑删除字段等,直接暴露给前端既不安全,也容易把前端的取值字段搞乱。我的做法是每个核心模块建一个*.vo包,用BeanUtils.copyProperties把 Entity 转成 VO。
5.2 联调时的四个高频问题
问题一:前端拿到的时间串不对
MySQL 里的datetime返回给前端时是2025-06-12T09:00:00这种带 T 的格式,直接显示非常难看。解决办法是在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8问题二:跨域请求失败
后端开启全局跨域配置后,仍然偶尔出现前端报跨域错误。大部分情况是 Spring Security 拦截了 CORS 预检请求(OPTIONS),需要显式放行:
http.cors().and().csrf().disable()问题三:分页插件失效
我之前用 MyBatis-Plus 分页插件,经常出现在“第一页正常,第二页数据重复”的情况。原因基本是分页插件没有正确注册到 MyBatis 的拦截器链里,在配置类中重新注入MybatisPlusInterceptor并添加分页插件即可。
问题四:Redis 缓存了脏数据
用户修改头像或昵称后,个人信息接口返回的还是旧值,因为缓存没有更新。这类问题没有捷径,必须统一封装缓存操作工具类,在“更新数据库”时主动删除缓存,并保证一套更新的操作同时执行。
6. 项目部署与优化扩展
6.1 部署方式:从本地到云服务器
这个项目我最终部署的方式是:一台 Linux 云服务器(2核4G),上面跑 MySQL、Redis、MinIO、Spring Boot Jar 包,前端 Vue 打包后的静态文件放在 Nginx 里,Nginx 做端口转发和反向代理。
关键部署步骤不复杂,但有几个坑要提前说:
- 不要把
application.yml里的配置写死,改成读取环境变量。否则每次换环境都要重新打包,非常愚蠢。 - MySQL 账号密码不要用 root/root 上生产,至少要单独建一个库账号并限制 IP。
- 服务器上跑 Jar 包时,一定要用
nohup java -jar xxx.jar > logs/out.log 2>&1 &这种后台启动方式,并用jps -l确认进程是否起来。
6.2 Docker 一键编排是否必要
如果你想让部署更优雅一点,或者想顺便展示一下 Docker 技能,那就用 Docker Compose 编排一套。
version: '3' services: mysql: ... redis: ... minio: ... backend: build: ./backend ports: "8080:8080" frontend: build: ./frontend ports: "80:80"注意:Docker 部署 Spring Boot 项目时,服务启动顺序不能随便编排,因为如果 MySQL 还没初始化好,后端启动就会报连接错误。我当时的做法是在启动脚本里加了一个简单的等待机制(延时 + 重试连接),比 Docker Compose 原生的depends_on更稳。
6.3 还能扩展哪些高价值功能
如果你的项目想更进一步,从“完成项目”变成“有亮点”,下面这几个方向可以选:
- 定时任务统计报表:每天凌晨统计订单数、营收数据,Spring 自带的
@Scheduled就能搞定。 - 消息通知:接入 WebSocket,用户下单后实时推送订单状态变化。
- 优惠券系统:后台发券、用户领券、下单抵扣。
- 多城市配置:服务根据用户定位展示不同城市的服务项目。
不过这些功能一定要按“增量”的思路做,不要一次性铺开,否则很容易把所有东西堆成半成品。我自己的习惯是:在核心链路稳定之前,绝不碰边缘功能。
7. 几个避坑指南与经验总结
7.1 通用避坑点
- 项目结构不要乱:controller/service/mapper/entity/vo/config 各司其职,别把业务逻辑写在 Controller 里,这种代码后面根本没法维护。
- 事务注解必须用对:下单操作涉及订单表、日志表甚至库存表,一定要在 service 方法上加
@Transactional(rollbackFor = Exception.class),默认只回滚RuntimeException,这点很多人会忽略。 - 日志别全用 System.out.println:至少用
@Slf4j输出日志,调试和排查线上问题时差别巨大。 - 敏感信息不要硬编码:数据库密码、JWT 密钥不要写在代码里,至少放配置中心或环境变量。
7.2 关于答辩和面试的准备
做这个项目过程中最有价值的不是把功能做完,而是能把每一步的设计理由说出来。面试官如果问“为什么用 Redis 做时段计数”,你要是能讲清楚“MySQL 在并发更新下锁竞争严重,而 Redis 的原子自增更轻量”这种深度对比,就已经超过大部分候选者了。
另外有一个高频问题是“订单超时未支付怎么办”。这个项目里我用的方案是:用户下单后写一张预支付订单,存到 Redis 缓存中并设置过期时间(比如 30 分钟),Redis 的 key 过期时触发一个监听回调,去关单或者放开名额。如果你的版本不想依赖 Redis,也可以用定时任务扫表,但实时性不如 Redis 方案。
类似这种细节,平时在写代码时有意积累三五个,简历和面试就都有了着落。