简介:这是一套基于SSM框架的家政保洁预约系统完整源码,面向计算机专业学生、Java初学者及需要课程设计或毕业设计参考的开发者,帮助解决家政服务在线预约、订单管理与后台维护等业务场景的实现问题。资源包共671个文件,约22.03MB,以152个Java后端源码、109个Vue前端组件、162个svg图标、60张jpg图片及44个js脚本为主,另含xml配置、sql建表脚本、properties配置与docx说明文档,覆盖前后端分离的完整工程结构。技术栈采用Java、SSM、Vue、Ajax、Maven与MySQL,配合MyBatisPlus与ElementUI,JDK1.8、MySQL5.7环境下可直接导入运行。目前已有191人学习下载。读者可获得用户信息管理、图片与视频素材维护等模块的可运行代码,以及数据库脚本、项目配置与目录组织范例,便于快速理解预约流程、二次开发与论文撰写参考。
1. 从一张 Excel 排班表说起:SSM 家政保洁预约系统到底解决什么问题
很多中小家政公司的调度台,本质上就是一张被反复涂改的 Excel:客户微信说“周六上午要两小时保洁”,调度员翻表找阿姨,打电话确认,再手写回填。单量一过 30 单/天,撞单、漏单、阿姨空跑就开始出现。这个标题里的“家政保洁预约系统”,要解决的就是把这条链路从人肉表格搬到线上:客户在线选服务类型、时段、地址下单,后台自动派单或人工派单,阿姨接单后回传状态,管理员看板统计。
技术栈锁定在 SSM(Spring + SpringMVC + MyBatis)+ Java,这是国内高校课程设计和中小企业外包项目里最常见的一套组合。它不新,但胜在资料多、上手快、部署轻,一台 2 核 4G 的云服务器就能跑。适合谁?一是要做课程设计、毕设的计算机专业学生,二是想给自家小家政公司做一套内部预约工具的小团队,三是想拿一个完整业务闭环练手 SSM 的 Java 初学者。下面我按“能跑起来、能改得动、能上线用”的顺序,把选型、建表、核心接口、避坑和进阶一条条讲清楚。
2. 选型与建模:为什么是 SSM,表该怎么切
2.1 SSM 三层分工与“为什么不用 SpringBoot”
SSM 的分工很清晰:Spring 管 Bean 和事务,SpringMVC 管 URL 路由和参数绑定,MyBatis 管 SQL 映射。相比 SpringBoot,SSM 少了自动装配,web.xml、applicationContext.xml、spring-mvc.xml、mybatis-config.xml这些配置文件都得自己写。听起来麻烦,但对学习者反而是好事——你能亲眼看到 DispatcherServlet 怎么接管请求、事务切面怎么织入。常见做法是:applicationContext.xml里配数据源、SqlSessionFactory、事务管理器;spring-mvc.xml里配注解驱动、视图解析器、静态资源放行;web.xml里注册 ContextLoaderListener 和 DispatcherServlet。
选 SSM 而不是 SpringBoot 的另一个现实理由:很多学校的实验环境、老服务器上的 JDK 还是 8,Tomcat 还是 8.5,SSM 的兼容性最稳。如果你的目标是快速上线而不是学习,那 SpringBoot 更合适;但如果标题就是 SSM,就老老实实把 XML 配全,别中途换栈。
2.2 数据库表设计:五张核心表撑起整个预约闭环
家政预约的业务实体不多,但关系要理清。核心是五张表:用户表、阿姨(服务人员)表、服务项目表、预约订单表、订单状态流水表。下面给出建表 SQL,字段名和类型可以直接抄。
-- 用户表:客户和管理员共用,用 role 区分 CREATE TABLE `sys_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT 'MD5加盐后的密码', `real_name` VARCHAR(30) COMMENT '真实姓名', `phone` VARCHAR(20) NOT NULL COMMENT '联系电话', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0客户 1阿姨 2管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 服务项目表:保洁类型、时长、单价 CREATE TABLE `service_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `item_name` VARCHAR(50) NOT NULL COMMENT '如日常保洁/深度保洁', `duration` INT NOT NULL COMMENT '服务时长(分钟)', `price` DECIMAL(10,2) NOT NULL COMMENT '单价', `status` TINYINT DEFAULT 1 COMMENT '1上架 0下架' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约订单表:核心表 CREATE TABLE `booking_order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', `user_id` INT NOT NULL COMMENT '下单客户', `staff_id` INT DEFAULT NULL COMMENT '接单阿姨,未派单为NULL', `item_id` INT NOT NULL COMMENT '服务项目', `service_date` DATE NOT NULL COMMENT '上门日期', `time_slot` VARCHAR(20) NOT NULL COMMENT '时段,如09:00-11:00', `address` VARCHAR(200) NOT NULL COMMENT '服务地址', `amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待派单 1已派单 2服务中 3已完成 4已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_staff_date` (`staff_id`,`service_date`,`time_slot`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;booking_order上的联合索引idx_staff_date是关键,派单时判断“这个阿姨这个时段有没有单”全靠它。order_no用业务号而不是自增 ID 对外暴露,避免被猜到单量。状态字段用 TINYINT 而不是字符串,查询和索引都更快。
2.3 派单冲突的判定逻辑
派单最容易翻车的地方是同一阿姨同一时段被派两单。判定逻辑不能只比service_date,还要比time_slot是否重叠。常见做法是把时段存成可比较的起止分钟数,或者干脆约定固定时段(如 08:00-10:00、10:00-12:00),用字符串相等判断即可。下面这段 MyBatis 查询用来检查冲突:
<select id="countConflict" resultType="int"> SELECT COUNT(*) FROM booking_order WHERE staff_id = #{staffId} AND service_date = #{serviceDate} AND time_slot = #{timeSlot} AND status IN (1, 2) <!-- 已派单、服务中才算占用 --> </select>参数说明:staffId是待派阿姨,serviceDate和timeSlot来自订单。注意status IN (1,2)——已完成和已取消的单不占用时段,否则阿姨的历史单会永久锁死她的排期。这个细节很多人第一次写会漏,导致阿姨接了几单后再也派不出去。
3. 核心接口实现:从下单到派单的完整链路
3.1 下单接口:参数校验与订单号生成
下单接口要处理三件事:校验时段是否可约、生成唯一订单号、落库。订单号我一般用“日期 + 随机数 + 用户 ID 后四位”,避免用 UUID 太长影响索引。下面给出 Controller 和 Service 的关键代码。
@RestController @RequestMapping("/api/booking") public class BookingController { @Autowired private BookingService bookingService; @PostMapping("/create") public Result create(@RequestBody @Valid BookingDTO dto, HttpSession session) { SysUser user = (SysUser) session.getAttribute("loginUser"); if (user == null) { return Result.fail("请先登录"); } // 校验时段是否已被该客户自己占用,防止重复提交 if (bookingService.hasSameSlot(user.getId(), dto.getServiceDate(), dto.getTimeSlot())) { return Result.fail("您在该时段已有预约"); } String orderNo = bookingService.createOrder(user.getId(), dto); return Result.ok(orderNo); } }@Service public class BookingServiceImpl implements BookingService { @Autowired private BookingOrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) public String createOrder(Integer userId, BookingDTO dto) { String orderNo = "BK" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + String.format("%04d", userId % 10000) + (int) (Math.random() * 9000 + 1000); BookingOrder order = new BookingOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setItemId(dto.getItemId()); order.setServiceDate(dto.getServiceDate()); order.setTimeSlot(dto.getTimeSlot()); order.setAddress(dto.getAddress()); order.setAmount(dto.getAmount()); order.setStatus(0); // 待派单 orderMapper.insert(order); return orderNo; } }逻辑说明:@Transactional保证插入失败时回滚;订单号里带日期便于按天分表或归档;hasSameSlot查的是同一客户同一时段,防止用户连点两次提交产生两单。参数上,serviceDate建议用@JsonFormat(pattern="yyyy-MM-dd")注解,否则前端传字符串后端解析容易报 400。
3.2 派单接口:并发下的时段占用
派单是并发敏感操作。两个管理员同时给同一阿姨派同一时段的单,如果只是“先查再插”,中间会有窗口期导致双派。稳妥做法是在booking_order上加唯一约束,或者用数据库行锁。我一般用“更新时带条件”的方式:
<update id="assignStaff"> UPDATE booking_order SET staff_id = #{staffId}, status = 1, update_time = NOW() WHERE id = #{orderId} AND status = 0 AND NOT EXISTS ( SELECT 1 FROM (SELECT 1 FROM booking_order WHERE staff_id = #{staffId} AND service_date = #{serviceDate} AND time_slot = #{timeSlot} AND status IN (1,2)) t ) </update>返回值是受影响行数,如果为 0 说明要么订单已被别人派了,要么阿姨该时段已被占用。Service 层根据返回值判断并给出提示。参数说明:orderId是待派订单,staffId是目标阿姨,serviceDate和timeSlot从订单里取。注意 MySQL 不允许在 UPDATE 的子查询里直接查同一张表,所以套了一层SELECT 1 FROM (...) t做临时表,这是血泪经验,不套会报 1093 错误。
3.3 状态流转与流水记录
订单状态从 0 到 3 的每次变更都应该留痕,方便对账和纠纷追溯。做法是加一张order_status_log表,每次更新状态时插一条记录。状态机要严格:待派单只能到已派单或已取消,已派单只能到服务中或已取消,服务中只能到已完成。用枚举或常量类约束,别在代码里散落魔法数字。
public enum OrderStatus { PENDING(0, "待派单"), ASSIGNED(1, "已派单"), SERVING(2, "服务中"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; // 构造、getter 省略 }状态流转校验放在 Service 层,更新前先查当前状态,不合法就抛业务异常。这样即使前端传了错误的状态值,后端也能拦住。
4. 避坑与排查:上线前必须过的五道坎
4.1 中文乱码:从表单到数据库全链路
现象:客户填的地址在数据库里显示成问号或乱码。原因通常是三处编码不一致:Tomcat 的URIEncoding、SpringMVC 的CharacterEncodingFilter、数据库连接的characterEncoding。解决:web.xml里加CharacterEncodingFilter设forceEncoding=true;JDBC URL 加?useUnicode=true&characterEncoding=utf8;建表用utf8mb4。三处都对齐后基本不会再乱。
4.2 事务不生效:Service 内部自调用
现象:下单时订单插入了,但流水没插,事务没回滚。原因是在同一个 Service 类里,A 方法直接调 B 方法,B 上的@Transactional不生效,因为没走代理。解决:把需要独立事务的方法拆到另一个 Service,或者注入自身代理。这是 SSM 里最经典的坑,面试也常问。
4.3 时段冲突漏判:只比日期不比时段
现象:同一阿姨同一天上午和下午各一单,系统却提示冲突。原因是判定逻辑只比了service_date没比time_slot。解决:冲突判定必须日期 + 时段一起比,且只统计status IN (1,2)的单。如果业务允许一天多单,时段字段的设计就要支持区间比较,别用字符串硬比。
4.4 MyBatis 返回 null:字段名与属性名不匹配
现象:查询订单列表,staffName一直是 null。原因是 SQL 里查的是s.real_name,但实体属性叫staffName,没配resultMap或别名。解决:要么在 SQL 里写s.real_name AS staffName,要么配resultMap做映射。开启mapUnderscoreToCamelCase=true只能解决下划线转驼峰,跨表别名还是得手动处理。
4.5 部署后 404:DispatcherServlet 拦截了静态资源
现象:接口能访问,但 CSS、JS、图片全 404。原因是spring-mvc.xml里配了<url-pattern>/</url-pattern>,所有请求都进了 DispatcherServlet。解决:加<mvc:resources mapping="/static/**" location="/static/"/>或<mvc:default-servlet-handler/>。前者更明确,后者交给容器默认 Servlet 处理,二选一即可。
5. 进阶技巧:用 POI 导出对账单,把系统用出价值
系统能下单派单只是及格线,真正让家政公司愿意用的是对账和统计。我一般会给管理员加一个“按月导出阿姨工时对账单”的功能,用 Apache POI 生成 Excel。POI 生成图表这件事,答案是能,但XSSFChart的 API 比较绕,日常对账用表格加汇总行就够了,图表交给前端 ECharts 更省事。
下面这段代码按阿姨汇总当月完成单量和金额,导出成 Excel:
public void exportBill(HttpServletResponse response, String month) throws IOException { List<BillVO> list = orderMapper.sumByStaff(month); // 按staff_id分组统计 XSSFWorkbook wb = new XSSFWorkbook(); XSSFSheet sheet = wb.createSheet(month + "对账单"); String[] headers = {"阿姨姓名", "完成单数", "总工时(小时)", "结算金额"}; XSSFRow head = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { head.createCell(i).setCellValue(headers[i]); sheet.setColumnWidth(i, 4000); } int rowIdx = 1; for (BillVO vo : list) { XSSFRow row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(vo.getStaffName()); row.createCell(1).setCellValue(vo.getOrderCount()); row.createCell(2).setCellValue(vo.getTotalHours()); row.createCell(3).setCellValue(vo.getAmount().doubleValue()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=bill_" + month + ".xlsx"); wb.write(response.getOutputStream()); wb.close(); }参数说明:month格式为2024-06,SQL 里用DATE_FORMAT(service_date,'%Y-%m')过滤;sumByStaff的 SQL 用GROUP BY staff_id聚合COUNT、SUM(duration)/60、SUM(amount)。注意Content-Disposition里的文件名如果含中文,要做 URL 编码,否则部分浏览器会乱码。导出接口要加权限校验,只允许管理员调用,别让客户能拉到全公司的结算数据。
验证方法很简单:造 10 条不同阿姨、不同状态的订单,跑一遍导出,核对 Excel 里的汇总数和数据库SELECT的结果是否一致。重点验证已取消的单有没有被算进去——sumByStaff的 WHERE 条件必须带status = 3,只统计已完成的单。
我自己踩过最深的一个坑,是早期图省事把状态判断写在了 Java 的 for 循环里,结果数据量一大就慢得离谱,后来全部改成 SQL 聚合才顺畅。做这类管理系统,能交给数据库算的就别在内存里循环,这是我做了几个项目后养成的习惯。希望帮到你。
本文还有配套的精品资源,点击获取