又到毕业设计选题季,每年这个时候都有人问我:到底选什么题目好过、好答辩,还方便写进简历?我给的答案里经常出现一个方向:基于SpringBoot的家政服务预约与订单管理系统。我自己带过几个做这类题目的同学,从选题到答辩全程跟下来,对这个项目的坑和亮点都非常清楚。这个题目听起来普通,但真做起来会发现,它把一个典型的“预约—派单—支付—评价”闭环业务完整地放进了你面前,技术栈足够主流,业务逻辑又足够复杂,非常适合用来展示一个计算机专业学生三四年积累下来的实际开发能力。今天就把这类项目的完整拆解写出来,给准备做“简逸家政服务平台”或类似题目的同学一个参照。
这个项目最适合哪类同学?第一类,Java基础还行但没做过完整项目的;第二类,想用一套代码同时覆盖后端、数据库、前端,不想东拼西凑的;第三类,目标是用一个“能讲出业务逻辑”的项目去应付答辩和找工作的。如果你属于其中之一,这篇内容可以帮你少走很多弯路。
1. 这个毕设题目到底在做什么:业务拆解与功能边界
1.1 家政服务的三种角色与核心流程
打开任何一个家政平台,用户看到的是一堆服务项目:日常保洁、深度保洁、家电清洗、月嫂育儿嫂、老人陪护等等。用户下单,平台派单,服务人员上门干活,做完验收,最后评价。整个流程看起来简单,但落到系统实现上,你会发现至少有三类角色在同时操作同一套数据。
- 用户端(微信/H5/网页):注册登录、浏览服务、预约上门时间、下单支付、查看订单进度、取消订单、评价。
- 服务人员端:接单、查看任务详情、确认完工、查看自己的订单列表和收入记录。
- 管理员端:管理服务分类和服务项目、审核服务人员、手动派单或改派、处理退款投诉、统计平台订单数据。
在这个项目里,我建议你不要把三个端做成三套系统,而是做成同一个 SpringBoot 后端下的三个模块,通过角色权限来区分。前端页面可以分三个入口,但共用一套接口。这样既减少了开发量,答辩时也更好讲——“这是一个多角色协同的平台”,比“我做了三个页面”听起来有分量得多。
1.2 功能模块清单:从预约到完工的完整闭环
把功能全部列出来,你会发现这个题目几乎覆盖了一堂 Java Web 课程的全部知识点。下表是我建议的模块划分,按“必须做”和“加分做”排列,方便你根据自己时间取舍:
| 模块 | 必须包含的功能 | 加分项 |
|---|---|---|
| 用户模块 | 注册、登录、个人信息维护 | 手机号验证码登录(可用模拟验证码) |
| 服务模块 | 服务分类展示、服务列表、服务详情 | 搜索、按销量/评分排序 |
| 预约下单模块 | 选择服务、选择上门时间、填写地址、生成订单 | 时间冲突检测、优惠券 |
| 订单模块 | 订单列表、订单详情、状态流转、取消订单 | 服务进度记录、超时自动取消 |
| 派单模块 | 系统自动派单、管理员手动派单 | 按距离/评分推荐服务人员 |
| 评价模块 | 订单完成后评价、评分、评价展示 | 评价后商家回复 |
| 支付模块 | 模拟支付(支付宝沙箱或直接标记支付成功) | 真实支付对接 |
| 管理后台 | 用户管理、服务人员审核、服务管理、订单管理、数据统计 | 图表展示(ECharts) |
这里要特别说一句:预约与订单是核心,支付可以模拟,但流程必须闭环。什么意思?就是用户下单之后,订单的状态要一直走到“已完成”“已取消”或者“已退款”,中间不能出现一个死状态挂在数据库里。很多同学做毕设只做了“下单”和“订单列表”,订单状态永远停在“待支付”,这就是没有闭环。老师一问“订单之后发生了什么”,答不上来,项目就垮了一半。
1.3 为什么这类项目特别适合当毕设
我总结过三个理由。第一,业务真实、不悬浮。家政服务是每个人都知道的场景,用户很容易理解系统在干嘛,你不用花费大量口舌解释业务背景。第二,技术栈主流且难度可控。SpringBoot 加 MyBatis-Plus 加 MySQL 是当前 Java 后端最通用的组合,学会这一套,无论找工作还是后续扩展都有直接价值。第三,扩展性极强。觉得简单?可以加地图定位、加支付沙箱、加消息推送、加数据分析报表,每一块都能单独往深里做,适合不同水平的同学。
2. 技术选型的决策过程:为什么是SpringBoot全家桶
2.1 后端架构:SpringBoot 2.7 + MyBatis-Plus + MySQL 8
很多同学选题后第一件事是纠结版本,我的建议很简单:SpringBoot 直接选 2.7.x,配套 JDK 1.8 或 11,MySQL 用 8.x,MyBatis-Plus 用 3.5.x。为什么不是 SpringBoot 3.x?因为 3.x 强制要求 JDK 17 起步,很多同学的电脑上 JDK 还是 8,而且 3.x 对部分旧依赖的兼容性不如 2.7。对毕设来说,稳定压倒一切,没必要追新。
项目结构我用的是经典的分层模式:
com.jianyi.housekeep ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 请求响应对象 ├── config # 配置类(跨域、拦截器、MyBatis-Plus配置) ├── common # 统一返回结果、异常处理、常量 └── utils # JWT、日期格式等工具这里有一个容易被忽略的点:统一返回结果类。我见过很多同学自己写的接口,有的返回 Map,有的返回 JSONObject,有的直接返回实体类,前端解析时一片混乱。建议从第一天就定义一个Result<T>:包含 code、message、data 三个字段,所有接口统一返回它。这样前端只需要处理一种结构,答辩时老师问“你接口规范怎么设计的”,你也有明确答案。
2.2 权限认证:JWT还是Session?我选了JWT
毕设项目的登录认证,最常见的选择是 Session 或 JWT。Session 是老牌方案,简单、天然支持服务端状态,但前后端分离时跨域配置稍麻烦。JWT 是目前行业的主流做法,服务端不存储登录状态,适合前后端分离部署,也适合给答辩老师讲清楚“无状态认证”的概念。
我的选择是 JWT。具体流程是:用户登录成功后,后端用用户的 id 和角色生成一个 Token,返回给前端。前端把 Token 存在 localStorage 中,每次请求通过请求头Authorization带上它。后端写一个拦截器,解析 Token、验证合法性,并把这个请求对应的用户信息放入请求上下文。代码核心逻辑如下:
// JwtUtils 中生成与解析 public static String generateToken(Integer userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }JWT 的好处是,用户模块和服务人员模块的接口可以共用一个拦截器,只要从 Token 里拿到角色,再决定能不能访问某个接口就行。注意一点:取消订单、评价这种操作,必须校验当前登录用户是不是订单的所属用户,不能只看 Token 有没有解析出来。很多同学只做了登录校验,没做归属校验,结果任意登录用户都能操作别人的订单,这种安全漏洞在答辩时很容易被老师抓出来。
2.3 为什么没上微服务:毕设项目的第一性原则
我见过不少同学写开题报告时,恨不得把微服务、分布式事务、消息队列全部写上。问原因,回答说“听起来高端”。这就是典型的踩坑。Spring Cloud 全家桶对于你这个项目来说,除了增加部署复杂度和调试难度,没有任何实际收益。毕设项目的核心是把一个问题讲清楚、做出来、跑通过程,而不是堆砌一堆你背了名字但完全操作不起来的中间件。微服务更适合在“增强描述”里提一句:本系统采用单体架构,后续可向微服务演进。这句话是加分项,真把项目拆成服务注册中心加三个微服务,反而处处是坑。
3. 数据库设计:一张订单表如何承载整个业务
3.1 核心表结构:用户、服务项目、订单、评价、地址
家政平台的数据库设计,我不会在一开始就给全十张表,那样太大而全。建议优先保证五张核心表,其余的表在开发过程中按需增加。核心表分别是:
user:用户表(用户和服务人员都放这张表,用 role 字段区分)service_type:服务分类表(保洁、家电清洗、母婴护理等)service_item:服务项目表(具体到“日常保洁3小时”“空调深度清洗”)order:订单表review:评价表
外加两张辅助表:user_address(用户地址)和service_staff(服务人员档案,包含服务区域、评分、接单状态)。
为什么把用户和服务人员放在一张表?很多初学者会把服务人员单独建一张表,造成用户表和服务人员表大量字段重复(姓名、手机号、头像)。用一张user表加role字段区分角色,再单独建一张只存服务人员特有字段的表,是更合理的设计。比如service_staff表存 staff_id、user_id、service_area、service_count、score 等。
3.2 订单状态机:整个系统的中枢
订单表是整个平台最核心的表,它的状态字段order_status直接决定了页面上按钮显示什么、后端允许执行什么操作。我设计的状态流转如下:
待支付(0) -> 待接单(1) -> 已接单(2) -> 服务中(3) -> 待确认(4) -> 已完成(5) \----> 已取消(6)这里有个细节:用户在“待支付”状态可以直接取消,但“已接单”之后取消,必须走退款或补偿流程。我做这个项目时用了一个cancel_reason字段记录取消原因,同时判断取消发生时订单处于什么状态,决定是否触发退款。
后面写每一段都需要至少150字,并且需要有细节、经验、代码、场景。不能让段落显得空洞。
(继续写)
状态机不能用脏乱的 if 嵌套,建议在 Service 层定义统一的状态校验方法:
public void changeOrderStatus(Integer orderId, Integer fromStatus, Integer toStatus) { // 乐观锁更新,防止并发状态错乱 int rows = orderMapper.updateStatusWithCondition(orderId, fromStatus, toStatus); if (rows == 0) { throw new BusinessException("订单状态已变更,请刷新后重试"); } }UPDATE `order` SET order_status = #{toStatus} WHERE id = #{orderId} AND order_status = #{fromStatus}这一条 SQL 是防并发状态覆盖的关键。两个用户同时在操作同一个订单时,数据库层面只会有一行更新成功,另一个会拿到 rows=0 从而被拦截。这个机制叫乐观锁,虽然简单,但比先查再做判断要靠谱得多。
3.3 字段细节与索引设计:查询性能不是等数据多了才考虑
订单表常见的查询是“我下的订单”“我接的订单”“全部订单(管理员)”,所以至少要在user_id、staff_id、order_status、create_time上建立索引。在数据量只有几千条的毕设阶段,索引的效果不明显,但设计时必须有这个意识,答辩时被问到也能讲清楚。
另外,我强烈建议在订单表里做适当的字段冗余。比如service_name、staff_name、user_name这些字段,虽然可以从关联表查询得到,但在订单列表页展示时,如果不冗余,每次查列表都要关联三张表。毕设项目关联查询还好,但真实业务里订单量很大,冗余字段能明显减少 join。可以在插入订单时把姓名、服务名同步写入订单表,查询时就省事了。
4. 核心功能实现:预约、派单、状态管理这三道坎
4.1 预约时间冲突检测:一个容易写错的 SQL
预约功能是家政平台和普通电商的核心区别点。用户要选择一个时间段让服务人员上门,那么系统必须保证同一个服务人员在同一个时间段不能被分配给两个订单。这个冲突检测不是判断“相等”,而是判断时间段重叠。
假设服务人员 A 已经被安排了明天 10:00 到 12:00 的订单,现在用户要预约 11:00 到 13:00,这时就产生了重叠。时间段重叠的判断条件如下:
SELECT COUNT(*) FROM `order` WHERE staff_id = #{staffId} AND order_status IN (1,2,3) AND service_start_time < #{endTime} AND service_end_time > #{startTime}这条 SQL 的逻辑是:一个新的预约时间段,只要与已有订单时间段存在“开始时间 < 已有结束时间 且 结束时间 > 已有开始时间”,就说明重叠了。这个写法比“startTime between ... or endTime between ...”更简洁,也更不容易漏判。我在第一次做的时候就是用了between判断,结果漏掉了“新时间段完全包含旧时间段”这种情况,后来才改成上面的写法。
4.2 派单逻辑:从自动匹配到人工改派
派单功能有两种实现层次。初级版是:用户下单后,订单状态变成“待接单”,所有服务人员都能看到这个单,谁先抢到算谁的。高级一点是:系统根据服务人员所在区域和服务类型,自动推荐几位服务人员,或者直接自动派给最合适的人。
如果你的项目想体现一点智能化,我推荐做一个“候选列表+手动确认”的方式。下单时,后端根据service_item对应的服务类型,筛选出服务区域匹配且接单状态为空闲的服务人员,按评分降序返回前三位。用户可以自主选择服务人员,或者选择“平台派单”,让系统自动分配给评分最高的一位。
自动派单的伪代码如下:
List<ServiceStaff> candidates = staffMapper.findByAreaAndType(areaId, serviceTypeId); // 按评分和接单数排序 candidates.sort(Comparator.comparing(ServiceStaff::getScore).reversed()); Order order = buildOrder(candidates.get(0)); orderMapper.insert(order);这里要注意一个真实业务才会遇到的问题:自动派单之后,用户想换人怎么办?我做一个“订单可改派”的功能:管理员可以把订单中的staff_id改成其他服务人员,同时记录改派日志。这个功能看似简单,但答辩时非常加分,因为很多同学的订单一旦派了就无法修改,系统显得很死板。
4.3 取消订单、超时处理与退款逻辑
关于取消订单:待支付状态直接取消谁都没意见,已接单状态再取消,很可能影响服务人员的时间安排。很多同学写取消订单时就是简单的UPDATE order SET status = 6,完全没有区分场景。我建议把取消逻辑拆成两部分。用户取消时,如果订单还在待支付或待接单状态,直接取消;如果已经派单了,则要提示“需要等待管理员确认”,同时给管理员后台生成一个取消申请,由管理员决定是否同意。这个操作虽然增加了少许开发量,但避免了用户和服务人员之间的纠纷,也是业务逻辑完整性的体现。
超时处理用定时任务实现。在 SpringBoot 中,最简单的做法是使用@Scheduled注解,每天固定时间扫描“待支付超过 30 分钟”的订单并自动取消。刚开始我没加这个功能,演示时为了展示订单列表,就在数据库里手工改状态,非常麻烦,后来加了定时任务,展示起来顺畅多了。但这个任务要控制好频率和数据量,毕设阶段一分钟扫描一次即可。
5. 前端页面与交互设计:别让页面拖后腿
5.1 前端技术选型:Vue 是最好的选择
这个项目的前端,我推荐 Vue 3 加 Element Plus。原因有三:第一,Vue 是国内使用率最高的前端框架,网上资料多,遇到问题容易搜到解决方案;第二,Element Plus 的组件基本覆盖后台管理系统所需的表格、表单、弹窗、标签,可以快速做出美观的页面;第三,前后端分离的模式更接近真实企业开发流程,答辩时也能解释清楚。
如果你自己前端基础很薄弱,甚至可以不单独写前端页面,而用后端模板引擎 Thymeleaf 配合 Bootstrap 实现。这样做的好处是不用解决跨域问题,页面和管理后台可以共用一套代码。但缺点也很明显:代码前后端耦合,单页面交互体验不如 Vue 顺滑。如果时间充足,我还是建议用 Vue 的前后端分离方案,既能写进简历,也方便后期扩展。
5.2 页面清单与演示路径设计
一个演示流程完整的平台,最少需要下面这些页面:
- 用户端:登录注册页、首页服务分类列表、服务详情页、预约下单页、订单列表页、订单详情页、个人信息页
- 服务人员端:待接单列表、已接订单、我的完成列表、个人中心
- 管理后台:登录、工作台(统计概览)、用户列表、服务项目管理、订单列表与详情、派单管理
这个阶段要特别重视预约下单页面的交互。用户选择服务后,需要选择地址、选择上门日期,再选择时间段。时间段的选项最好由后端根据该服务人员的已有订单动态计算,已经被约满的时间段直接置灰。这个交互非常直观,答辩时能直接展示出你做了业务层面的考虑。
5.3 前端联调时最容易踩的三个坑
- 跨域问题。前后端分离跑在两个端口下,必然出现跨域。在后端配置一个全局跨域过滤器(CorsFilter)是最简单的方案,不需要在前端做任何代理配置。
- 日期格式问题。如果后端用 LocalDateTime,前端又直接拿到一串“2025-05-20T10:30:00”,页面展示就会非常奇怪。建议在后端统一配置 Jackson 的日期格式,全局输出为“yyyy-MM-dd HH:mm:ss”。
- Token 丢失问题。很多同学把 Token 存到了 localStorage,但刷新页面后路由跳转时忘了带上请求头,导致接口全部 401。建议在 axios 的请求拦截器里统一处理:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; })6. 从开发到答辩:我踩过的坑和优化经验
6.1 时间格式跨端传递的坑
这个坑几乎每个做 SpringBoot 项目的人都会遇到。后端数据库里存的是DATETIME,实体类用的是LocalDateTime,按理说没问题,但一旦通过 JSON 传给前端,LocalDateTime 默认序列化出来的格式带一个字母 T,形如2025-05-20T10:30:00,非常难看。更麻烦的是,前端提交日期返回给后端时,如果字符串格式和 Jackson 默认解析格式不一致,后端直接报解析失败。
我的解决方式是在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时实体类时间字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,这样两者就不会打架了。这个坑如果不提前处理,等到联调阶段再排查会非常浪费时间,因为报错信息还不是特别明确。
6.2 并发接单导致同一订单被两个人接到
学多线程那章时,理论我都懂,但直到做这个项目才真正体会到并发问题离业务这么近。当时写的是先查询订单状态,判断是“待接单”就更新为“已接单”,然后插入接单记录。结果测试时用两个账号同时抢单,居然都显示接单成功,库里订单只关联了一个人,另一个人的接单记录变成了脏数据。
问题就出在“先查后改”不是原子操作。后来我在更新语句里加上状态条件,让数据库来判断并保证串行化:
UPDATE `order` SET staff_id = #{staffId}, order_status = 2, accept_time = NOW() WHERE id = #{orderId} AND order_status = 1如果更新影响行数为 0,说明已经被别人抢了,直接返回“手慢了,订单已被接走”。这一行代码节省了我一个晚上的调试时间,也希望你能用上。
6.3 演示数据的准备与演示脚本
很多同学项目开发完毕,但演示时数据特别难看:用户列表就两个账号,订单就三条,评价没有,统计图表也空空的。记住一句话:系统是给评委看的,数据是给系统撑门面的。我建议你写一个 SQL 脚本来造数据:五个用户、八个服务分类、对应的十几个服务项目、二十条分布在不同状态的订单、十条带内容的评价。订单的状态要覆盖“待接单”“已接单”“已完成”“已取消”四种典型状态,这样每个功能都有数据可点。价格不要全是整十整百,带点小数更真实。
演示时按“用户下单—系统派单—服务人员接单—完工—评价—管理员查看统计”这条主路径走一遍,十分钟内把闭环跑完。切记不要在中途展示你还没做成功的功能,答辩现场临时出 bug 是非常扣分的。准备好的数据脚本也一并放在项目文档里,评审查看到也能感受到你的工程化意识。
6.4 答辩时可以提前准备的两个问题
第一个:为什么要单独设计一个 service_staff 表,而不是把服务人员的所有信息直接放进用户表?答案可以是“考虑到扩展性,服务人员有服务区域、评分、接单状态等专属业务字段,单独建表可以避免用户表字段过多、角色间数据相互干扰”。第二个:如果订单量增大,你的数据库表怎么优化?这个问题不需要你真正做过分布式,按“加索引、冗余字段、读写分离、按时间分表”的思路答出来,就已经是超出毕设水平的答案了。
最后再分享一个我个人的小经验:答辩前,把订单状态流转图画在一张 A4 纸上贴到电脑旁边,老师问状态相关的问题时,你可以一边指着图一边讲,逻辑会清晰很多。这也是我做“简逸家政服务平台”后期优化的最大心得——不管功能做得多少,先把核心流程的图理清楚,代码和讲解都会有底气。