ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM家政保洁预约系统实战:从Excel排班到自动派单的完整实现

SSM家政保洁预约系统实战:从Excel排班到自动派单的完整实现 简介这是一套基于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与ElementUIJDK1.8、MySQL5.7环境下可直接导入运行。目前已有191人学习下载。读者可获得用户信息管理、图片与视频素材维护等模块的可运行代码以及数据库脚本、项目配置与目录组织范例便于快速理解预约流程、二次开发与论文撰写参考。1. 从一张 Excel 排班表说起SSM 家政保洁预约系统到底解决什么问题很多中小家政公司的调度台本质上就是一张被反复涂改的 Excel客户微信说“周六上午要两小时保洁”调度员翻表找阿姨打电话确认再手写回填。单量一过 30 单/天撞单、漏单、阿姨空跑就开始出现。这个标题里的“家政保洁预约系统”要解决的就是把这条链路从人肉表格搬到线上客户在线选服务类型、时段、地址下单后台自动派单或人工派单阿姨接单后回传状态管理员看板统计。技术栈锁定在 SSMSpring SpringMVC MyBatis Java这是国内高校课程设计和中小企业外包项目里最常见的一套组合。它不新但胜在资料多、上手快、部署轻一台 2 核 4G 的云服务器就能跑。适合谁一是要做课程设计、毕设的计算机专业学生二是想给自家小家政公司做一套内部预约工具的小团队三是想拿一个完整业务闭环练手 SSM 的 Java 初学者。下面我按“能跑起来、能改得动、能上线用”的顺序把选型、建表、核心接口、避坑和进阶一条条讲清楚。2. 选型与建模为什么是 SSM表该怎么切2.1 SSM 三层分工与“为什么不用 SpringBoot”SSM 的分工很清晰Spring 管 Bean 和事务SpringMVC 管 URL 路由和参数绑定MyBatis 管 SQL 映射。相比 SpringBootSSM 少了自动装配web.xml、applicationContext.xml、spring-mvc.xml、mybatis-config.xml这些配置文件都得自己写。听起来麻烦但对学习者反而是好事——你能亲眼看到 DispatcherServlet 怎么接管请求、事务切面怎么织入。常见做法是applicationContext.xml里配数据源、SqlSessionFactory、事务管理器spring-mvc.xml里配注解驱动、视图解析器、静态资源放行web.xml里注册 ContextLoaderListener 和 DispatcherServlet。选 SSM 而不是 SpringBoot 的另一个现实理由很多学校的实验环境、老服务器上的 JDK 还是 8Tomcat 还是 8.5SSM 的兼容性最稳。如果你的目标是快速上线而不是学习那 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 服务项目表保洁类型、时长、单价 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下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约订单表核心表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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 idcountConflict resultTypeint 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(patternyyyy-MM-dd)注解否则前端传字符串后端解析容易报 400。3.2 派单接口并发下的时段占用派单是并发敏感操作。两个管理员同时给同一阿姨派同一时段的单如果只是“先查再插”中间会有窗口期导致双派。稳妥做法是在booking_order上加唯一约束或者用数据库行锁。我一般用“更新时带条件”的方式update idassignStaff 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设forceEncodingtrueJDBC URL 加?useUnicodetruecharacterEncodingutf8建表用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做映射。开启mapUnderscoreToCamelCasetrue只能解决下划线转驼峰跨表别名还是得手动处理。4.5 部署后 404DispatcherServlet 拦截了静态资源现象接口能访问但 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 更省事。下面这段代码按阿姨汇总当月完成单量和金额导出成 Excelpublic void exportBill(HttpServletResponse response, String month) throws IOException { ListBillVO 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;filenamebill_ month .xlsx); wb.write(response.getOutputStream()); wb.close(); }参数说明month格式为2024-06SQL 里用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 聚合才顺畅。做这类管理系统能交给数据库算的就别在内存里循环这是我做了几个项目后养成的习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表