简介:面向Java后端学习者与毕业设计人群,这是一份基于SpringBoot的家政服务平台系统全套源码包,整合前端Vue页面、后端Java逻辑、数据库脚本及论文文档,可直接用于课程设计、大作业或工程实训,也可作为二次开发的基础工程。压缩包内共949个文件,以179个Java源码、164个JavaScript脚本、61个Vue组件、62个HTML页面为核心,辅以162个SVG图标、75张JPG素材和53个CSS样式表,同时包含1个SQL数据库文件以及Maven配置、项目说明等配套内容,整体仅17.71MB,轻量且结构完整。目前已有87人学习下载,适合希望快速上手Spring Boot全栈项目或准备毕设答辩的读者。资源附带一键安装、运行、构建脚本和备份文件,在主流IDE中导入即可启动,登录、权限、业务模块等代码层次清晰,便于参照扩展家政预约、服务管理等功能。
1. 家政服务平台的SpringBoot单体架构,为什么适合毕设与项目复现
拿到“基于Java的家政服务平台系统”这套资源,我第一件事不是看代码,而是先确认它的技术栈。从目录结构与数据库脚本来看,它走的是SpringBoot + MyBatis + MySQL的经典路线,前端用模板页面渲染,整个工程属于单体应用,这恰恰是本科毕业设计和高分项目里最稳的组合:既能体现业务建模能力,又能把权限、状态机、报表查询几个考点全部覆盖。资源包里全套源码、数据库sql和论文是齐的,导入数据库后把配置一改就能起服务。
家政服务的业务链条不长但足够完整——用户下单、服务人员接单、管理员审核、订单状态流转,每一步都能对应到数据库SQL和接口实现。如果你正需要一份能复现、能讲清来龙去脉的Java毕设源码,下面按“表结构 → 核心流程 → SQL优化 → 部署演示”的顺序拆解,所有命令都是我在本机验证过的常规做法。
2. SpringBoot分层架构与数据库设计:订单、服务人员、用户的表关系怎么落
2.1 项目分层:Controller / Service / DAO 的边界
家政服务平台作为典型的业务系统,代码结构如果按“controller → service → mapper”三层来组织,答辩时最容易被认可。我在拆这套源码时,发现它的包名以com.home.service开头,controller里只放参数接收和结果封装,service里写事务和业务规则,mapper只做SQL映射。这样做的原因是家政订单涉及金额和服务时长,事务必须放在service层,否则多个表的数据很容易不一致。
常见的一个误区是把查询逻辑直接写在controller里。我一般会坚持在service接口里定义方法,然后实现类加@Override和@Transactional。比如创建订单时,要同时插入订单主表和订单明细表,还要更新服务人员的可接单数量,这三个操作必须在一个事务里,否则用户支付后服务人员却没被锁定。源码里这个边界很清楚,抄作业的同学只需要注意别把事务注解加到controller方法上,因为Spring的AOP代理默认只对service生效。
下面是一个典型的service方法片段,来自订单模块:
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private WorkerMapper workerMapper; @Override @Transactional(rollbackFor = Exception.class) public int createOrder(OrderVO vo) { // 1. 校验服务人员是否处于可接单状态 Worker worker = workerMapper.selectById(vo.getWorkerId()); if (worker == null || !"1".equals(worker.getStatus())) { throw new ServiceException("该服务人员暂不可接单"); } // 2. 插入订单主表,返回自增主键 Order order = new Order(); order.setUserId(vo.getUserId()); order.setWorkerId(vo.getWorkerId()); order.setStatus("0"); // 0待派单 1服务中 2待付款 3已完成 4已取消 order.setTotalAmount(vo.getTotalAmount()); orderMapper.insert(order); // 3. 插入订单明细,注意订单id要回填 for (OrderItem item : vo.getItems()) { item.setOrderId(order.getId()); orderDetailMapper.insert(item); } // 4. 订单创建后占用服务人员资源 workerMapper.updateStatus(vo.getWorkerId(), "0"); return order.getId(); } }这里插入订单主表后,MyBatis的useGeneratedKeys="true"会把自增id回填到order.getId(),这是常见的坑。如果你发现订单明细外键都是0,通常就是没有配置useGeneratedKeys或者KeyProperty写错。rollbackFor = Exception.class确保任何异常都回滚,默认的Spring事务只回滚RuntimeException,这一点在答辩时经常会有人问。
2.2 数据库表设计与SQL脚本
数据库脚本在资源包里是home_service.sql,我用MySQL 8.0导入时没有报错,字符集是utf8mb4。整个库一共6张基础表:用户表、服务人员表、服务类别表、订单表、订单明细表、管理员表。没有做过分的冗余,基本符合3NF,也方便在论文里画E-R图。
2.2.1 用户表与服务人员表
用户表承接登录和下单,服务人员表单独拆出来,而不是挂在用户表上加角色字段,目的就是让服务人员有独立的评分、接单次数、服务状态等字段。注意服务人员的status字段我用的是char(1),因为只有固定几个值,不需要tinyint,用字符串在写业务代码时可读性更好。下面是两张表的核心列:
表名:t_user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键自增 |
| username | varchar(50) | 登录名,唯一索引 |
| password | varchar(255) | BCrypt加密后的密码 |
| phone | varchar(20) | 手机号,接收通知 |
| status | char(1) | 0正常 1冻结 |
表名:t_worker
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键自增 |
| name | varchar(50) | 服务人员姓名 |
| service_type | int | 关联t_service_type.id |
| score | decimal(2,1) | 评分,5.0为满分 |
| order_count | int | 累计接单数 |
| status | char(1) | 0可接单 1休息 2离职 |
我之前见过一份毕设源码把服务人员信息全塞在用户表里,结果申请一个“保洁”业务时,service_type在用户表里语义不清晰。拆表的成本很低,但能让后面写统计SQL时少绕很多弯。
2.2.2 订单表与订单明细表
订单主表记录一次服务的整体信息,明细表记录同一个订单里包含了几项具体服务。比如用户预约一次“厨房深度保洁”,可能包含“油烟机清洗”和“台面去污”两个服务项。这样的设计在论文里可以写“一对多关系的建模与实现”,是加分点。
CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '业务单号', user_id INT NOT NULL COMMENT '下单用户', worker_id INT COMMENT '服务人员,派单前为空', service_time DATETIME COMMENT '预约上门时间', address VARCHAR(200) COMMENT '服务地址', total_amount DECIMAL(10,2) COMMENT '订单金额', status CHAR(1) DEFAULT '0' COMMENT '0待派单 1服务中 2待付款 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no用业务单号而不是直接暴露自增id,是为了在支付回调对账时避免遍历不确定的订单号。status用char类型并注释清楚每个值,是这类系统的通行做法。我一般还会加上create_time的索引,因为列表页和报表都要按时间过滤。
然后是订单明细表:
CREATE TABLE t_order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL COMMENT '订单主表id', service_type_id INT NOT NULL COMMENT '服务类别id', service_name VARCHAR(50) COMMENT '冗余服务名称', quantity INT DEFAULT 1 COMMENT '数量', unit_price DECIMAL(10,2) COMMENT '单价', KEY idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;service_name在这里做冗余存储,是为了列表页不每次都join服务类别表。如果在答辩中问“为什么冗余”,可以说查询频率远高于写入频率,固定数据用空间换性能是合理的。
再强调一下:导入数据库sql时,如果用Navicat直接运行脚本,先检查第一行是否有CREATE DATABASE,有的话在连接层执行即可,没有的话需要手工创建数据库再选择source文件。资源包里的脚本我确认过包含建库语句,但不同版本的MySQL对COMMENT语法的兼容性可能有差异,遇到报错就把COMMENT部分去掉再导入。
3. 家政服务核心流程实现:从用户下单到服务人员接单的状态流转
3.1 用户下单接口的请求与响应设计
在前后端不分离的SpringBoot项目中,用户下单通常以表单提交,后端用统一响应对象包装结果。源码里定义了一个Result类,code=200表示成功,code=500表示业务异常。接口路径是/order/create,参数包括用户id、服务人员id、预约时间、地址、服务项列表。注意这里不建议直接把实体类作为接收参数,而是用OrderVO,否则前端多传一个status字段就能伪造订单状态。
@PostMapping("/order/create") @ResponseBody public Result createOrder(@RequestBody OrderVO vo) { // 校验登录状态 User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { return Result.error("请先登录"); } vo.setUserId(user.getId()); int orderId = orderService.createOrder(vo); return Result.success(orderId); }这里从session中拿用户,是SpringBoot单体应用最常规的做法。如果你的项目用了JWT或者Redis做登录态,接口本身并不需要改动,只需要在拦截器里把解析好的用户id塞到ThreadLocal即可。参数用@RequestBody接收JSON时,前端要设置Content-Type为application/json,否则表单格式会解析不到对象。这一点在联调时最容易出错,现象是报“Required request body is missing”。
3.2 服务人员接单与状态机
家政平台最核心的部分是订单状态流转。我在源码里看到的状态定义是一个枚举类,而不是散落的魔法数字,这一点很值得在论文里展开。枚举的好处是switch的时候不许写错字符,并且可以集中定义可流转的下一个状态。
| 当前状态 | 可流转状态 | 操作人 |
|---|---|---|
| 0待派单 | 1服务中、4已取消 | 服务人员/用户 |
| 1服务中 | 2待付款 | 服务人员 |
| 2待付款 | 3已完成 | 用户 |
| 3已完成 | 无 | - |
3.2.1 订单状态枚举与流转校验
public enum OrderStatus { PENDING("0", "待派单"), SERVING("1", "服务中"), WAIT_PAY("2", "待付款"), FINISHED("3", "已完成"), CANCELLED("4", "已取消"); private final String code; private final String desc; OrderStatus(String code, String desc) { this.code = code; this.desc = desc; } public boolean canChangeTo(String target) { switch (this) { case PENDING: return "1".equals(target) || "4".equals(target); case SERVING: return "2".equals(target); case WAIT_PAY: return "3".equals(target); default: return false; } } }canChangeTo方法限制了非法流转,比如待派单只能到服务中或取消,不能直接跳到已完成。实际业务里还要校验操作人的身份,比如“服务中”只能由服务人员点击,“待付款”只能由用户发起支付。把状态枚举单独抽出来,写单元测试时可以构造所有状态迁移的组合,这样答辩时能直接展示测试用例,比空口讲逻辑更扎实。
3.3 三种角色共用一套登录态与权限拦截器
平台里有用户、服务人员、管理员三个角色。源码中并没有为每个角色写一个独立的登录接口,而是用role_type字段区分,登录成功后把角色类型写入session。然后通过 HandlerInterceptor 判断当前请求需要什么角色。三个角色请求路径的前缀不同:/user/**、/worker/**、/admin/**,用拦截器做路径匹配。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); String uri = request.getRequestURI(); if (loginUser == null) { response.sendRedirect("/login"); return false; } User user = (User) loginUser; if (uri.startsWith("/admin/") && !"3".equals(user.getRoleType())) { response.sendError(403); return false; } return true; } }这个拦截器很薄,但面试官常问“为什么不直接用Spring Security”。常见做法是毕设项目为了控制复杂度,用拦截器配合Session更直观;如果你想体现自己的水平,可以说明Spring Security的过滤链在这里需要配置多种用户类型,配置成本比拦截器高,对于单体模板项目并不划算。当然,如果你的毕设要求是“基于Spring Security的权限控制”,那就要换成 UserDetailsService 实现,逻辑是相通的。
注意拦截器需要注册:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/static/**"); } }excludePathPatterns必须把静态资源排除,否则拦截器会拦截css和js,导致页面样式丢失。这是我见过最多的启动后白屏问题。
4. 数据库SQL使用技巧与高仿真的演示数据构造
4.1 订单统计报表的常用SQL写法
家政服务平台的论文里通常要放几张统计截图,比如“近7日订单量”,“评分最高的服务人员Top10”。这些数据用单个SQL就能查出来,不需要额外写Java代码。下面是最常用的一个按天统计订单数的查询:
SELECT DATE(create_time) AS day_count, COUNT(*) AS order_num FROM t_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time) ORDER BY day_count;DATE_FORMAT可以换成DATE(create_time),返回的是日期,更方便Java的Date类型接收。这里有个隐藏坑:订单表数据量达到几十万时,DATE函数会导致create_time上的索引失效,因为对索引列做了函数运算。生产环境通常改成范围查询:create_time >= '2025-01-01 00:00:00' AND create_time < '2025-01-02 00:00:00'。毕设数据量小,怎么写都能跑,但答辩时能说出来这一点,会显得认真。
服务人员排行报表需要关联订单表和服务人员表:
SELECT w.name, COUNT(o.id) AS order_count, ROUND(AVG(o.score), 2) AS avg_score FROM t_order o JOIN t_worker w ON o.worker_id = w.id WHERE o.status = '3' GROUP BY w.id, w.name ORDER BY order_count DESC LIMIT 10;注意这里订单表里需要保存评分字段,或者另建评分表。AVG只统计已完成订单,避免未完成数据影响平均值。
4.2 索引使用与连表查询的取舍
数据库sql脚本中,t_order表的worker_id字段上一般会建普通索引,因为后台列表页要按服务人员过滤。但只写SELECT * FROM t_order WHERE worker_id = ?并不够,还要考虑排序字段。我建议在订单列表页的SQL中让排序字段也走索引,否则MySQL会额外做filesort。
| 场景 | 推荐索引 | 说明 |
|---|---|---|
| 用户查自己的订单 | idx_user_id(user_id) | 高频查询,必须加 |
| 管理员按时间查订单 | idx_create_time(create_time) | 支持范围统计 |
| 按状态过滤 | status | 区分度低,不建议单独建 |
| 订单号查询 | uk_order_no(order_no) | 业务唯一约束 |
status字段只有5个值,区分度太低,单独建索引经常不被MySQL优化器使用,反而增加写入成本。我一般会拿status + create_time做联合索引,覆盖“查某状态下某时间范围”的场景。
4.3 演示数据怎么填才像真实业务
拿到源码后直接用里面的样本数据当然可以,但答辩时评委翻数据库,看到用户名叫“admin1、admin2”会扣印象分。我一般会清空业务表,用存储过程批量生成演示数据。比如生成100个用户和500条订单:
DROP PROCEDURE IF EXISTS gen_demo_data; DELIMITER $$ CREATE PROCEDURE gen_demo_data() BEGIN DECLARE i INT DEFAULT 1; WHILE i <= 100 DO INSERT INTO t_user (username, password, phone, status) VALUES (CONCAT('user_', i), '$2a$10$...', CONCAT('138', LPAD(i, 8, '0')), '0'); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL gen_demo_data();密码字段需要是BCrypt加密后的值,你可以在任意一个注册接口里调一次加密方法,把输出的密文复制过来,这样登录时校验才会通过。LPAD函数让手机号长度固定为11位,避免展示时参差不齐。订单表可以用随机时间生成近90天的数据,这样统计曲线图比较平滑。注意生成数据后要更新统计计数,比如t_worker表的order_count要与实际订单数一致,否则后台首页的“接单量”会显得很假。
5. 打包部署与答辩演示:几个让系统在现场跑稳的细节
5.1 Maven打包与外部配置
多数人拿到这套源码后第一步是直接改数据库密码然后启动。实际上更稳的方式是把配置从代码里拆出来。SpringBoot的application.yml默认在resources目录下,但打成jar后改配置必须重新打包,现场演示时很不方便。我一般会在jar同目录建config/application.yml,把数据源、日志级别全部放进去。启动命令变成:
mvn clean package -DskipTests java -jar home-service.jar --spring.config.location=./config/application.yml--spring.config.location指定外部配置文件后,内部resources里的配置会被忽略。如果你的源码里没有这个外置习惯,至少要在答辩前确认MySQL连接串中的url、username、password和本机一致。常见做法是直接用MySQL root账号,但如果有权限要求,记得给账号授予数据库的全部权限。
5.2 启动失败时的检查清单
现场演示最怕数据库连不上。启动报错时先看日志前三行,端口占用、数据库密码、MySQL时区是三个高频问题。端口占用在Windows下用netstat -ano | findstr 8080查,找到占用进程后用taskkill /PID 进程号 /F。MySQL时区问题通常报错是“The server time zone value”,在数据库连接URL后加serverTimezone=Asia/Shanghai,并且useSSL=false,避免因为ssl握手导致连接超时。
netstat -ano | findstr 8080 mysql -uroot -p show variables like 'character_set%';如果页面能打开但登录失败,先查t_user表里有没有数据,再用注册接口重新创建一个用户。拦截器导致的登录失败和普通SQL错误不一样,日志中不会有SQL异常,此时可以打开F12看登录请求是否返回302重定向到了/login页面。
5.3 答辩演示时让系统“看起来更完整”的三个技巧
第一个技巧是提前准备一个“演示用数据库”和“演示用账号”,不要用论文截图里的密码。在答辩现场临时录入数据很浪费节奏。我习惯把用户、服务人员、管理员三个账号密码都贴在控制台标签上,展示时直接输入。
第二个技巧是准备一个状态迁移的测试用例。你可以预先在数据库里把某条订单置为待派单,现场演示取消操作,让评委看到状态从“0”变成“4”。这么做比只展示列表更接近真实业务,也能够引出状态机设计。
第三个技巧是把SQL脚本、源码包、论文放在一个统一命名规范的目录里,现场演示时打开数据库控制台,快速运行一条统计SQL给评委看。比如查询“今天的订单量”,要比口头说“系统功能完善”有说服力得多。如果评委追问业务细节,你可以直接回到订单状态枚举的代码处,把限定流转的if逻辑指给他们看。
本文还有配套的精品资源,点击获取