最近在整理手头的毕业设计和课程设计选题时,又看到家政服务类网站的高频出现。这类题目看起来不起眼,但真要在限定时间内做出一套能跑、能看、能演示的系统,不少同学还是会卡在需求分析和订单状态设计上。这篇文章以编号28555的某县域家政服务平台为案例,完整梳理设计思路和核心实现。地区与机构名称我不具体点,统一用某县某诚家政代称,思路是通用的,只要把服务项目、计价方式、角色权限换掉,就能直接搬到你自己的选题里去。
家政服务这个领域,和图书管理、学生管理系统相比有个明显不同:它天然就是三方角色参与的。业主端要找人干活,服务人员端要接单赚钱,管理员要管项目和订单。如果只做一个简单的发布服务页面,演示时完全看不出工作量,也很难写进论文。真正能把项目撑起来的,是订单状态流转和角色间的协作逻辑。这篇就是从这两个核心点展开。
适合看这篇文章的人,我把话说直白些:一是正在做类似选题、想找完整设计思路的同学,二是已经会写基础增删改查、但想搞明白“订单状态怎么设计不容易乱”“接单时怎么防止两个人抢同一单”这类真实业务问题的开发者。看完你可以直接把表结构、状态机、接口逻辑抄到自己的项目里,再按自己的需求改一改。
1. 项目整体定位与需求拆解
1.1 这类家政平台到底在解决什么问题
县域家政市场的现状,和一二线城市不太一样。大城市的家政平台拼的是阿姨背景调查、在线支付、售后保障,县城和乡镇更多还是靠熟人介绍和电话联系。电话联系有几个痛点:服务价格不透明,同一个保洁项目,问三个人能报出三个价;服务人员的时间安排全在脑子里,约好了临时放鸽子也没处说理;做完服务没有评价反馈,干得好坏全靠运气。
某诚家政这个平台,本质上就是把“电话联系”这件事搬到线上。业主在网站上浏览服务项目、查看服务人员档期、在线下单;服务人员收到订单提醒后接单,合理安排自己的时间;管理员在后台统一管理服务项目、审核人员和查看订单数据。平台不碰线下服务本身,只做信息匹配和订单管理,这样就把业务边界画清楚了。
我在给项目做技术方案时,一直提醒自己一个问题:这是给谁用的。如果只站在管理员角度设计,页面会很单调;只站在业主角度设计,服务人员端就被忽略了;三方角色都照顾到,项目才是一个完整的闭环,演示的时候也更有说服力。
1.2 用户角色与功能边界划分
这个项目一共分三种角色,功能和边界一开始就得划清楚,否则后边编码时会反复改表。
业主端,也就是普通用户,核心操作是:注册登录、浏览家政服务分类、查看服务项目详情、下单预约、在线支付或线下付款、查看自己的订单状态、对已完成订单进行评价。
服务人员端,核心操作是:接单、更新订单服务状态、管理自己的服务项目(比如我擅长保洁和家电清洗,就上架这两个,不擅长的不上架)、查看与业主的历史沟通留言。
管理员端,核心操作是:用户管理(禁用异常账号)、服务项目管理(上架、下架、改价)、服务人员审核与派单例外处理、订单查询与统计、评价内容审核。
角色边界这块我强烈建议用矩阵图整理出来,哪怕画在纸上也行。我见过不少同学做项目,用户表里放一个role字段就完事了,登录后所有人看到一样的菜单,这就是典型的边界没划清楚。本题设计时,我用了一个最直接的方式:登录后根据角色跳转到不同首页模块,每个角色的菜单都是独立的一套。
1.3 功能需求分析的三个隐藏点
除了上面这些明面上的功能,有三个点很容易被忽略,但这恰恰是项目答辩时老师最爱追问的。
第一个是服务人员的时间冲突问题。一个保洁阿姨同一时间段不能被两个订单同时占用,这就引出了接单时的并发校验,后面我会单独讲实现方案。
第二个是订单的取消与退款。业主下单后服务人员还没接单时,业主可以无条件取消;服务人员接了单,业主再取消,就要分情况处理,比如扣取一定比例的违约费用,或者进入管理员审核。
第三个是服务项目的二级分类。很多人做成一张单表,服务分类和服务项目混在一起,后边加一级分类时要改表结构。正确做法是设计成父子关系,比如一级分类是“日常保洁”,下面挂“日常保洁2小时”“日常保洁4小时”“深度保洁”这些具体项目,计价方式也不一样,有的按次收费,有的按面积收费,计价字段必须跟着服务项目走。
2. 技术选型与系统架构设计
2.1 为什么选 Spring Boot + MyBatis 而不是别的东西
每次选技术栈,我都建议先想清楚一个前提:这个项目最终的目的是什么。课程设计和毕业设计,评审老师看的是完整性和业务逻辑,不是看用了多新的框架。用 Spring Boot + MyBatis + MySQL,最大的好处是生态成熟,出了问题搜一下就有答案,团队里的人也都熟悉。
对比过其他方案:用纯 JSP + Servlet,代码量会爆炸,而且前后端混在一起维护困难;用 Node.js + Express 做后端,对刚接触企业开发的同学来说反而不容易理解事务管理和依赖注入这些概念;用现成的 RuoYi 这类后台管理脚手架,导入就能跑,但项目里很多代码是脚手架自动生成的,写论文时很难解释清楚每一块是自己写的,答辩容易被问倒。
我选 Spring Boot 还有一个实际理由:内置 Tomcat。以前做 SSM 项目,手动配置 Tomcat 的坑太多了,端口冲突、部署路径不对、XML 配错一个标签整个项目起不来。Spring Boot 直接打成 jar 包运行,本地开发和线上部署都省事。MyBatis 相比 JPA,优势在于 SQL 可控。家政平台的订单查询涉及多表关联、条件筛选、统计汇总,SQL 写起来很灵活,MyBatis 的这种自由度在报表统计时价值很大。
这个项目的具体版本组合,我放在后面实操章节给出一份可以直接用的清单。
2.2 前后端分离还是服务端渲染
现在的开发趋势是前后端分离,Vue + Spring Boot 满天飞。但这个项目我坚持选择了服务端渲染,也就是 Thymeleaf 模板引擎 + BootStrap,这个决定很多同学不理解,我说说原因。
家政平台是面向三类用户的业务系统,核心诉求是信息展示和表单提交,不是复杂的实时交互。模板渲染的方式,一个请求返回一个完整页面,浏览器不用做二次请求,演示时网络环境不稳定也不会出现接口 404 这种尴尬情况。
另外,前后端分离意味着要同时维护两套工程、处理跨域问题、写接口文档。对于周期紧张的课程设计,成本偏高。我见过太多同学 Vue 前端做好了,后端接口还没写完,最后答辩时只能展示半成品。服务端渲染虽然“土”,但是开发效率极高。页面和接口在同一个工程里,改起来一步到位。
当然,我也留了一个妥协方案:页面中局部交互简单的用原生 JavaScript 和 jQuery 处理,比如下单时的地址弹窗、评价时的星级点击。这不影响整体架构,又避免了每个页面都刷新跳转的笨重感。
2.3 系统架构与请求流程
系统的整体分上下四层:Controller 接收请求、Service 处理业务、Mapper 操作数据库、Domain 封装实体。网上喜欢把这叫分层架构,其实就是让每层代码负责自己的事,Controller 里不写 SQL,Mapper 里不写业务判断。
目录结构我是这样组织的:
src/main/java/com/ancheng/platform ├── controller(页面控制器和接口控制器) ├── service(业务逻辑接口和实现) ├── mapper(MyBatis持久层) ├── domain(实体类) ├── interceptor(登录拦截器、管理员拦截器) ├── config(配置类,如拦截器注册) └── common(统一结果返回、常量类、状态枚举)请求流程走的是标准 MVC:浏览器请求到达 Controller,Controller 从 Session 中获取当前登录用户信息,再调用 Service 处理业务,Service 里通过 Mapper 与 MySQL 交互,最终把处理结果返回到视图层,Thymeleaf 渲染成 HTML 输出。
这套架构看似简单,但在后期加功能时优势就出来了。比如我要新增一个“服务人员手工派单”功能,只需要在 Controller 加一个方法、Service 加对应逻辑、Mapper 加一条 SQL,不用动其他模块。模块之间耦合度低,是我看重它的最大价值。
3. 数据库设计:核心表与关系梳理
3.1 核心表清单与设计依据
数据库设计是整个项目的地基。表设计不好,后边写 SQL 时每一条都别扭。这个平台我最终设计了 9 张表,这里把核心的列出来说明用途。
| 数据表名 | 功能说明 | 核心字段 |
|---|---|---|
| t_user | 用户表,三种角色统一存储 | id, username, password, real_name, phone, avatar, role, status |
| t_category | 服务一级分类表 | id, name, sort, status |
| t_service | 服务项目表,挂在分类下面 | id, category_id, name, price, price_type, unit, describe, cover_img, status |
| t_service_skill | 服务人员技能表,关联服务项目和人员 | id, user_id, service_id, status |
| t_order | 订单主表,核心业务表 | id, order_no, user_id, worker_id, service_id, service_time, address, contact_name, contact_phone, price, status, cancel_reason, create_time |
| t_comment | 评价表 | id, order_id, user_id, worker_id, service_id, score, content, reply, create_time |
| t_consult | 订单沟通留言表 | id, order_id, send_user_id, content, create_time |
| t_favorite | 业主收藏服务项目表 | id, user_id, service_id, create_time |
| t_banner | 首页轮播图表 | id, img, url, sort, status |
有些字段没有一一列出,比如 create_time、update_time 这类公共字段,每张表都要有,目的是排查数据问题时知道这条数据是谁在什么时候创建的。我见过不少同学的库把订单表建得简短精干,没了时间字段,统计月度订单量时才发现没有数据可查。
3.2 订单表:最重要的状态机设计
订单表是整个系统的核心,其中 status 字段是灵魂。我把订单状态设计成了八个,用数字常量来表示:
| 状态值 | 状态名 | 含义与流转 |
|---|---|---|
| 0 | 待接单 | 业主下单成功,等待服务人员接单;此时业主可取消 |
| 1 | 已接单 | 服务人员接单,服务时间和地点已确认;此时双方不可随意取消,取消要走管理员审核 |
| 2 | 服务中 | 服务人员点击“开始服务”,表示人已到达地点 |
| 3 | 待确认 | 服务人员点击“完成服务”,等待业主确认 |
| 4 | 已完成 | 业主确认服务完成,订单结束,可发起评价 |
| 5 | 已取消 | 订单被业主取消或超时未接单自动取消 |
| 6 | 已退款 | 取消或申请退款后钱款退回给业主 |
| 7 | 待审核 | 订单取消申请进入管理员审核状态,审核通过后转为已退款或已取消 |
这个状态机的核心价值在于:不是所有状态之间都能直接跳转。比如服务中不能直接变成已完成,必须经过待确认,让业主确认后才能闭环,这样处理纠纷时有记录可查。很多同学状态设计只有一个“未完成/已完成”,项目一引入取消和退款立刻崩盘,问题就出在这里。
数据一致性上,我用了两个手段。第一,在 Service 层写状态变更方法时,SQL 里显式加上“当前状态必须等于某个值”的 WHERE 条件,例如将状态从 0 改为 1 时,SQL 里同时写 status = 0,如果影响行数为 0,就说明这个订单已经被别人处理过了,直接抛出异常。第二,状态枚举类统一放在 common 包里,代码里不允许出现魔法数字,这样代码可读性提升很多。
3.3 用户与服务项目的多对多关系
一个服务人员可以做保洁、也可以做家电清洗,一个服务项目也可以被多个服务人员承接,这就是多对多关系。数据库通过 t_service_skill 这个桥表来解耦。
service_skill 表设计时还有一个业务点:是否被管理员审核通过。服务人员自己上传技能,管理员审核通过后才生效,避免服务人员乱挂自己干不了的项目。这个审核动作虽然小,但是在答辩时可以讲成“业务流程上的风控设计”,是完整的闭环思维。
评价表的另一个细节我要强调:订单和评价是一对一关系,我在 t_order 表里加了一个 is_commenced 字段,标识这个订单是否已经评价过。否则同一订单可以无数次评价,评分数据就会失真。评价表里冗余了 worker_id 和 service_id,虽然可以从订单表查出来,但冗余的目的是为了后续统计服务人员的综合评分时,不需要每次 join 订单表。
4. 核心功能实现与关键代码
4.1 用户登录与角色权限控制
登录功能看似简单,但权限控制是很多项目的薄弱点。这个项目我没有引入 Spring Security,原因和选型时一样:对于三种固定角色,用拦截器完全可以解决,Spring Security 的配置和学习成本在这个场景里收益不高。
我在拦截器里做了两级校验。第一级是登录校验:拦截所有 /user/、/worker/、/admin/** 路径,Session 中不存在 user 对象就跳转登录页。第二级是角色校验,针对不同子路径分别要求角色常量,比如访问 /admin/** 必须是 role=0 的管理员,这样即使普通用户拼路径访问后台也会被拦下来。
public class LoginInterceptor implements HandlerInterceptor { private static final Integer ROLE_ADMIN = 0; private static final Integer ROLE_WORKER = 1; private static final Integer ROLE_USER = 2; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } String uri = request.getRequestURI(); if (uri.startsWith("/admin/") && !ROLE_ADMIN.equals(user.getRole())) { response.sendRedirect("/index"); return false; } if (uri.startsWith("/worker/") && !ROLE_WORKER.equals(user.getRole())) { response.sendRedirect("/index"); return false; } return true; } }密码存储方面,我用的是 MD5 加固定盐再做一次加密。虽然现在更推荐 BCrypt,但课程设计项目里很多同学还在用明文密码,MD5 加盐至少比明文强很多。这段代码如果放到生产环境,直接换成 PasswordEncoder 的 BCrypt 实现即可,思路一样。
4.2 下单与接单:事务和并发怎么处理
下单流程不是简单的 INSERT 一条订单记录就完了,它背后要做三件事基础校验:服务项目必须处于上架状态、顺手把最新价格查出来塞进订单(防止前端改价格提交)、订单的服务时间不能与当前已接订单冲突。
时间冲突校验是这里比较复杂的地方。一个服务人员的服务安排是区间性质的,比如 2025-07-20 14:00 到 16:00 保洁。我要校验新订单时间是否与已有订单重叠,SQL 大致思路是这样的:
SELECT COUNT(*) FROM t_order WHERE worker_id = #{workerId} AND status IN (1, 2, 3) AND service_time = #{serviceTime} AND #{endTime} > service_time AND service_time + INTERVAL duration_minute MINUTE > #{beginTime}这个 SQL 用重叠区间的条件判断,可以准确识别时间重叠。如果 count 大于 0,就直接返回提示“该时间段已被预约”。
接单环节的并发冲突更隐蔽。两个业主同时下单了同一个服务人员的同一个时间,服务人员刷新接单页,看到两条订单,手快点了第一条,再点第二条时系统要能拦住。我在更新订单状态的 SQL 里做了一个乐观锁式的状态条件:
int rows = orderMapper.updateStatusByIdAndOldStatus(workerId, orderId, OrderStatus.WAITING.getCode(), OrderStatus.ACCEPTED.getCode()); if (rows == 0) { throw new RuntimeException("订单已被其他人抢单,请刷新查看最新状态"); }如果更新行数为 0,说明这条订单已经在极短时间内被别人处理了,事务回滚,当前请求直接提示失败。这种方法性能开销小,纯靠数据库行锁就能保证,逻辑也比 Redis 分布式锁简单太多,适合这个项目的定位。
同时,Service 方法上加 @Transactional,保证更新状态和写入操作日志原子生效,不会出现状态改了但日志没写的半成品数据。
4.3 订单状态的推进与评价闭环
订单状态的推进,我用了一个统一的 Service 方法封装,而不是在每个 Controller 里直接操作状态字段。先看逻辑:
public void changeOrderStatus(Integer orderId, Integer operatorId, Integer oldStatus, Integer newStatus, String operateRole) { Order order = orderMapper.selectById(orderId); if (order == null) { throw new RuntimeException("订单不存在"); } // 权限校验:业主只能确认或取消,服务人员只能接单、开始、完成 checkPermission(order, operatorId, newStatus, operateRole); int rows = orderMapper.updateStatusByIdAndOldStatus( orderId, oldStatus, newStatus); if (rows == 0) { throw new RuntimeException("订单状态已变化,请刷新后重试"); } }评价闭环这块,我设计为“订单状态为已完成且 is_commenced 为 false 时才可评价”,提交评价用一条 SQL 同时做两件事:插入评价记录 + 更新订单的评标记。为什么强调要同时?因为如果分两步做,先插评价、再更新状态,中间如果抛了异常,用户就会感觉“评价提交了但是订单还是显示可以去评价”,数据就乱了。
这里附上一条核心的更新 SQL,用场景去理解更清楚:
UPDATE t_order SET is_commenced = 1, status = 4 WHERE id = #{orderId} AND status = 3 AND is_commenced = 0affected rows 为 1 才说明这次确认有效。接着 Insert 评价数据,整个流程放在一个事务里,保证评价和状态变更同生共死。
5. 实操部署与功能验证
5.1 开发环境与项目初始化
说实话,很多同学的代码本身没大问题,最终挂在环境配置上。这个项目我这边用的环境组合比较稳,分享出来直接照抄:
- JDK 1.8
- Maven 3.6.3
- Spring Boot 2.7.18
- MyBatis 2.3.1
- MySQL 5.7(8.0 也可以,注意驱动版本即可)
- Thymeleaf 模板引擎
- Bootstrap 4.6 前端样式库
创建项目时我用的 Spring Initializr 生成骨架,导入 IDE 后,先配置 application.yml。这里有一个必坑点:数据库连接 URL 里如果没有加上 characterEncoding=utf-8,后边控制台打印中文时可能一堆问号。正确配置参考:
spring: datasource: url: jdbc:mysql://localhost:3306/ancheng_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false encoding: UTF-8项目里我特意把 Thymeleaf 的缓存关掉了,因为开发阶段需要频繁改页面模板,开着缓存,刷新页面看到的永远是旧内容,排查效率极低。等到真正上线部署,再把 cache 改为 true 也不迟。
5.2 核心接口自测记录
我在开发过程中始终保持一个习惯:后端接口写完,先用 Postman 测试,再通过页面端到端验证。举两个典型的自测场景。
第一个是下单接口。我先注册一个业主账号,给一个存在的时间段下单,正常返回 200。接着故意构造一个已被占用时间段去下单,返回“该时间段已被预约”的错误提示。再看数据库里 t_order 的状态为 0,说明 Insert 和校验都正确。
第二个是并发抢单。写了一个小脚本模拟两个并发请求同时更新同一订单,发现第一个请求成功,第二个返回“订单已被其他人抢单”。这个结果才是符合预期的,如果两个都成功,毫不夸张地说,这个项目上线第一天就会出严重问题。
5.3 部署上线与静态资源处理
本地开发没问题后,上线部署主要三步。第一步 Maven 打包,clean package;第二步把生成的 jar 放到服务器,执行 java -jar ancheng-platform.jar 启动;第三步在服务器安全组放行 8080 端口。如果是用的传统外置 Tomcat,就要把打包方式调成 war,并且本地 Tomcat 的路径配置需要小心。
静态资源上传是家政平台必然遇到的需求:业主上传头像、服务人员上传技能图片、后台轮播图管理。我在 upload 目录做了统一配置,并开放了静态资源映射。这里最容易被坑的是 Windows 本机和 Linux 服务器的路径差异。本地可能用的是D:/upload/,Linux 上是/usr/local/upload/。我建议把上传路径写成项目配置项,部署时改配置文件即可,不要硬编码在代码里。
开发时我遇到过一次诡异的情况:页面图片上传成功,刷新页面后图片却消失了。排查半天发现是 Spring Boot 开发环境的热部署把静态资源目录也重置了。所以我自己后来养成了一个习惯——上传路径放在项目运行目录外的独立磁盘目录,避免被重启误删,正式部署也方便备份。
6. 常见问题与排查心得
6.1 我踩过的五个坑
做一个全栈小项目,坑基本集中在配置和并发上。这里整理一下我踩过的高频问题,按发生概率排个序。
| 现象 | 根因 | 解决方法 |
|---|---|---|
| 表单提交后台收到中文乱码 | 项目编码与页面编码不一致 | 统一所有文件为 UTF-8,数据库连接加 characterEncoding |
| 接口方法明明调用了事务却未生效 | Service 方法被同类内部调用,代理未生效 | 事务方法拆到独立 Service,或类外部调用 |
| 图片上传后页面无法访问 | 静态资源映射未配置 | 注册 WebMvcConfigurer 统一映射上传目录 |
| 两个请求同时接单都成功 | 没有对旧状态做条件限制 | 更新 SQL 加 status 条件,影响行数为 0 则抛异常 |
| 同一订单重复评价多次 | 缺少评价标记位限制 | 订单表加 is_commenced 字段,事务中更新 |
事务不生效这个问题很隐蔽,我简单解释一下原理:Spring 的事务是基于 AOP 代理实现的,外部调用 A 方法时,A 方法上加 @Transactional 才有效;如果你在同一个类里,一个 B 方法内部调用了 A 方法,这个调用是 this.a(),走的是当前实例而不是代理对象,事务就失效了。遇到这种情况,把需要事务的方法独立到另一个 Service 类中,或者注入自身代理对象再调用。
6.2 慢查询与索引优化实录
项目数据量不大,但我在做管理员统计报表时还是遇到了一次明显的卡顿。统计页面要跨三张表关联用户、订单、服务项目,再按月份分组汇总,第一次写出来跑了半天,数据库 CPU 一路飙高。
我用 EXPLAIN 看执行计划,发现 t_order 表走了全表扫描。问题根源是查询条件里有 user_id、worker_id、status、service_time 四个维度,但表里只有一个主键索引。后来我针对高频查询组合建了索引:
ALTER TABLE t_order ADD INDEX idx_user_status_time (user_id, status, service_time); ALTER TABLE t_order ADD INDEX idx_worker_status_time (worker_id, status, service_time);加完索引后,查询响应时间从原来的一秒以上降到几十毫秒。给同学们的教训是:索引不是越多越好,但订单这种查询维度固定的业务表,针对高频检索字段建联合索引是非常值得的投资。
6.3 演示前最后检查的清单
这类项目到了答辩演示环节,很多问题会在关键时刻暴露。我给自己定制了一个演示前检查清单:
- 数据库服务是否启动,连接信息是否正确。
- 项目端口是否被占用,日志中最后有没有出现异常堆栈。
- 初始化数据是否存在,管理员账号、测试业主账号、服务人员账号是否能登录。
- 首页轮播图片路径是否有效,图片是否存在。
- 上传目录的图片在演示机上能否访问,是否因为路径不同导致 404。
- 演示用的核心流程是否预演过:登录→下单→接单→开始服务→完成服务→确认→评价。
- 手机上演示时,页面布局是否错乱,因为评审经常拿手机看。
每一次演示前,我至少会把核心流程完整跑两三遍。尤其是接单环节,我习惯准备一个备用账号,防止演示现场主账号突然 Session 过期或者被挤下线。
结尾
这类县域家政平台项目,做完一遍下来,真正锻炼人的不是增删改查,而是三个地方:需求边界怎么拆分、订单状态机怎么设计、并发冲突怎么处理。你要是只是对着视频敲了一遍代码,没有自己重新设计订单表、没有模拟两个人同时抢单的场景,那这个项目练手的价值至少打了一半折扣。
我个人在实际操作中的体会是:家政平台这个选题非常适合拿来练手,业务足够贴近生活,功能复杂度适中,三个角色天然适合讲权限控制和业务闭环。做完基础版之后,还可以往三个方向扩展提升:增加微信小程序端,把业主下单入口搬到手机里;对接在线支付,把支付回调处理加进订单状态机里;接入接单消息推送,让服务人员不用一直刷新页面。这些扩展方向每个都不难,但能明显看出一个人的工程意识。
最后分享一个小技巧:源码获取之后,不要急着跑起来,先看订单表的 status 字段,再看状态变更的 SQL。能看懂状态机,这个项目你才算是真正拿到手了。