“你这套家政服务系统能不能跑起来看个效果?”——坐在我对面的同学问这话时,桌面上的 Spring Boot 正在控制台里刷日志,MySQL 里数据已经灌好了,浏览器还停在 JSP 页面上转圈。他做的是技术选型,我做的是收尾。说实话,这类“基于 Java + Spring Boot + MySQL + JSP + Layui”的管理系统,在毕业设计和课程设计里出现频率极高,但很多人做出来的东西只是把 CRUD 拼了一遍,真正拆开看,订单状态怎么流转、权限怎么拦截、Layui 表格怎么和 JSP 配合,一堆细节全是坑。
我这次完整复盘一下这套家政服务系统的设计与实现。它不是什么高并发大流量项目,但胜在业务链路完整:用户能注册登录、浏览服务、下单预约,管理员能维护服务项目、处理订单、审核评价,还带着 JSP 服务端渲染和 Layui 组件交互。我会把需求拆分、数据库设计、后端核心逻辑、前端页面组织方式,以及我实际跑项目时踩过的坑全放出来。无论你是拿它当毕设参考,还是想自己搭一套带后台管理的小系统,这套思路都能直接用。
1. 内容整体设计与思路拆解
1.1 家政服务系统到底要解决什么问题
家政服务系统的业务逻辑,说穿了就是把线下那套“客户打电话预约、公司手工记单、最后回访确认”的流程搬到线上。传统模式下最大的痛点是信息不透明:用户不知道有哪些服务项目、价格是多少、服务人员什么时候能上门,家政公司这边接单全靠登记表,订单一多就容易漏单、错单,服务质量也没有办法追踪。
所以在设计系统之前,我先把业务流程捋了一遍:用户浏览服务分类和项目,选择需要的服务后填写预约时间、服务地址、联系电话,提交订单;后台管理员能看到新订单并及时处理,确认后进入服务流程;服务完成后用户可以对本次服务进行评价。就这样一条主线,衍生出了用户管理、服务分类管理、服务项目管理、订单管理、评价管理这几个核心模块。
权限上也分成两条线:普通用户走前台,能下单、查订单、写评价;管理员走后台,负责服务内容和订单的维护。两边的页面入口和数据操作范围完全分开,这就需要在后端做一套简单的角色权限控制,不能只靠前端隐藏按钮来拦。
1.2 技术栈选型:Spring Boot + MySQL + JSP + Layui 的组合逻辑
这套技术栈放在今天来看确实显得“复古”,但组合在一起其实非常合理。Spring Boot 解决了传统 SSM 项目里大量 XML 配置的问题,内嵌 Tomcat,打一个 jar 包就能跑,对没有太多部署经验的学生来说友好得多。MySQL 是大家最熟悉的数据库,表结构设计和 SQL 调试都有大量现成资料可以参考。这两块承担了系统的核心后端和存储能力。
JSP 和 Layui 的选择,则是从前后端分离还没那么普遍、毕设评审又偏爱“能看到完整页面渲染过程”这个角度考虑的。JSP 作为服务端模板引擎,可以直接在页面里用 JSTL 和 EL 表达式拿后端数据,避免了前后端联调时接口对不上的麻烦。Layui 的优势在于它自带表格、分页、弹窗、表单校验这些后台管理高频组件,而且把 CSS 和 JS 封装得比较完整,不用自己写复杂的 JavaScript 交互代码,对于非前端专业的人来说极其友好。
有人可能会问,为什么不直接用 Vue + Element UI?不是不能用,而是对于这套系统的体量来说,引入 Vue 意味着要构建前端工程、配置跨域、处理异步加载,整个链路拉长不少。用 JSP + Layui 的组合,页面渲染稳定,开发效率高,也符合很多学校对 Java Web 项目的验收习惯。
1.3 系统整体架构和模块划分
系统整体分为前台展示端和后台管理端,两者共用同一套 Spring Boot 后端服务和 MySQL 数据库。前台是给普通用户用的,包括用户注册登录、服务分类浏览、服务项目列表、服务详情、在线下单、个人中心(我的订单、个人信息修改)这几个页面。后台则是给管理员用的,包括管理员登录、用户列表管理、服务分类管理、服务项目管理、订单列表管理、评价列表管理等。
从代码层面看,遵循的是经典的三层架构:Controller 负责接收请求和参数校验,Service 层处理业务逻辑,Mapper(DAO)层通过 MyBatis 操作 MySQL。这种分层的好处是职责清晰,一旦某个环节出问题可以快速定位。比如订单状态流转这种频繁核心变更的逻辑,我只在 Service 层里改方法体,Controller 和数据库表都不用动。
另外,我对 JSP 文件的目录也做了规划。前台页面放在 /WEB-INF/jsp/front/,后台页面放在 /WEB-INF/jsp/admin/,这样以后维护的时候只要看路径就知道是哪个端的页面。静态资源(Layui、CSS、图片)统一放到 src/main/resources/static/ 下,Spring Boot 会默认映射这个目录。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 用户与管理员表设计
用户表是整个系统的根基,所有下单和评论操作都挂在用户 ID 上。我设计的 user 表包含这些核心字段:id(主键自增)、username(登录名,唯一)、password(MD5 加密后的密码)、nickname(昵称)、phone(手机号)、address(默认服务地址)、avatar(头像)、status(账号状态,1 正常 / 0 禁用)、create_time(注册时间)。
这里有个容易忽略的点:密码一定要加密存储。我用的 MD5,生产环境建议换成 BCrypt,毕设里用 MD5 加个固定盐也能过关。另外 username 字段必须加唯一索引,否则注册的时候会出现两个相同用户名,业务上没法判断到底是谁。
管理员表 admin 结构类似,字段包括 id、username、password、real_name(真实姓名)、role(角色,区分超级管理员和普通管理员)、create_time。管理员账号是系统初始化时直接往数据库里插一条记录,不开放注册入口,避免有人混入后台。
2.2 服务分类与服务项目表设计
服务分类表 service_category 用于管理保洁、保姆、月嫂、维修等大类,字段有 id、name(分类名称)、icon(图标地址)、sort(排序权重,数字越小越靠前)、status、create_time。sort 这个字段很多人设计时会漏掉,结果分类一多顺序就乱了,加一个排序字段可以灵活调整展示顺序。
服务项目表 service_item 是用户真正下单时对应的具体服务,字段包括 id、category_id(关联分类表)、name(服务名称,比如“日常保洁 3 小时”)、cover(封面图)、price(价格,用 DECIMAL(10,2))、unit(计费单位,比如“次/小时”)、description(服务说明)、status(是否上架)、create_time。
category_id 上建立普通索引,因为前台列表页经常要根据分类 ID 查询服务项目。price 使用 DECIMAL 而不是 FLOAT,MySQL 中浮点数做金额计算容易产生精度问题,尤其后续如果要算总价或者做统计,DECIMAL 能保证两位小数准确。
2.3 订单表与评价表设计
订单表是整个系统业务逻辑的核心,字段比较多,需要仔细设计。我的 orders 表包含了这些字段:id、order_no(订单编号,唯一)、user_id(下单用户)、service_item_id(服务项目)、service_date(预约日期)、service_address(服务地址)、contact_name(联系人)、contact_phone(联系电话)、remark(订单备注)、status(订单状态)、create_time、update_time。
这里最关键的设计是订单状态 status,我用 0、1、2、3、4 分别表示“待接单、已接单、服务中、已完成、已取消”。这个状态机是整个订单流转的中枢,所有状态变更我都放到 Service 层统一处理,确保非法状态切换被拦截掉。
评价表 comment 关联订单,字段包括 id、order_id(对应订单)、user_id、rating(评分,1-5 分)、content(评价内容)、reply(管理员回复)、create_time。我特意把评价和订单关联而不是和服务项目直接关联,这样可以限制只有完成过订单的用户才能评价,防止刷评价。
各表之间的关系可以简单梳理成:用户 1 对多订单,服务分类 1 对多服务项目,服务项目 1 对多订单,订单 1 对 1 评价。设计关系时不需要过度冗余,只要能满足功能查询即可。
2.4 MySQL 连接与字符集配置
数据库层面还有一个容易踩坑的地方是连接配置。我用的 Spring Boot 2.7 版本,application.properties 里的数据源配置必须注意时区和编码,不然启动后查询时间会差 8 小时,插入中文变成问号。我的连接串是这样写的:
properties复制spring.datasource.url=jdbc:mysql://localhost:3306/housekeeping?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
建库的时候统一使用 utf8mb4 字符集,排序规则用 utf8mb4_general_ci。utf8mb4 是 utf8 的超集,能完整支持中文和特殊符号,现在大部分场景推荐直接用这个。另外 MySQL 8.x 和 5.7 的驱动类名不一样,8.x 需要用 com.mysql.cj.jdbc.Driver,5.7 用 com.mysql.jdbc.Driver 就行,这个务必看仔细。
3. 后端核心功能实现
3.1 Spring Boot 项目初始化与目录结构
创建 Spring Boot 项目时,我直接在 IDEA 里用 Spring Initializr 生成的,Java 版本用的 8,Spring Boot 版本选的 2.7.x。这里有个重要原因:jsp 在 Spring Boot 中的支持需要引入额外的依赖,版本太高(比如 3.x)对 JSP 的原生支持反而更折腾,而且很多资料都是基于 2.x 写的,遇到问题好查。
pom.xml 里除了 spring-boot-starter-web 之外,必须额外添加两个依赖:tomcat-embed-jasper 用来编译 JSP,jstl 用来在 JSP 中写 JSTL 标签。不加的话,启动后访问 JSP 页面会报 404 或者直接白屏。同时记得把打包方式设置成 war,虽然内嵌 Tomcat 支持 JSP,但打成 war 包部署起来更稳。
项目目录结构按职责划分成五块:controller 放接口入口,service 和 service.impl 放业务逻辑和实现,mapper 放 MyBatis 的 Mapper 接口,entity 放实体类,config 放拦截器、WebMvc 配置等。JSP 文件统一放在 src/main/webapp/WEB-INF/jsp/ 下,这个目录不受静态资源映射影响,外部直接访问不了,必须通过 Controller 跳转,安全性好一些。
3.2 MyBatis 集成与通用 CRUD 实现
持久层我用的 MyBatis,集成方式很简单:引入 mybatis-spring-boot-starter 依赖,然后在启动类上加上 @MapperScan("com.example.mapper") 注解,扫描 Mapper 接口。实体类直接用驼峰命名法,同时开启 Spring Boot 的驼峰映射配置,这样数据库里的下划线字段 create_time 能自动映射到实体的 createTime,省去大量写映射文件的功夫。
properties复制mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=classpath:mapper/*.xml
具体写 Mapper 时,我没有用 MyBatis-Plus 的自动 CRUD,而是手写 XML 里的 SQL。原因是毕设项目里 SQL 都不复杂,手写一来能加深对 SQL 的理解,二来答辩时老师问起来能说得清楚。比如无条件分页查询服务项目列表,XML 里就是一条简单的 select * from service_item where status = 1 limit #{offset}, #{pageSize}。
分页这里有一个经验:别在 DAO 层去拼 LIMIT 参数,建议在 Service 层统一计算 offset = (pageNum - 1) * pageSize,然后作为参数传入 SQL。这样换个分页插件也方便,逻辑也更清晰。
3.3 登录鉴权与权限拦截实现
登录功能我用的是 Session + 拦截器的方式。用户登录成功后,把用户对象放进 Session,同时在 Session 里存一个标记字段 userRole 来区分普通用户和管理员。每次请求过来,拦截器先检查 Session 里有没有这个标记,没有就跳转到登录页,有就放行。
拦截器配置方面,我写了两个拦截器类分别处理前台和管理后台。前台拦截器只拦截 /order/**、/user/** 这类需要登录才能访问的路径,/home、/service/list、/service/detail 这些公开页面直接放行。管理后台拦截器拦截 /admin/** 下的所有请求,并且会多判断一层角色。
这里有一个很多新手容易踩的坑:拦截器会把静态资源也拦了。如果访问 Layui 的 CSS 和 JS 文件时被重定向到登录页,页面就会一直转圈然后报 404。必须在拦截器配置文件里显式排除静态资源路径,比如 /static/**、/layui/**。
3.4 订单流程的核心逻辑与状态机控制
订单流程是系统里业务逻辑最密集的地方,我把它拆成几个方法:createOrder(创建订单)、acceptOrder(接单)、startService(开始服务)、completeOrder(完成订单)、cancelOrder(取消订单)。每个方法内部先校验当前状态是否合法,再更新状态并刷新 update_time。
创建订单的步骤比较有代表性:第一步从 Session 中拿到当前登录用户 ID,第二步校验服务项目是否存在且处于上架状态,第三步组装订单数据生成唯一的 order_no,第四步插入数据库,最后跳转到订单详情页。生成 order_no 我用的是 yyyyMMddHHmmss + 6位随机数 的方案,够用而且不会太复杂。
状态机控制的关键在于非法操作的拦截逻辑。举个例子,用户只能取消“待接单”状态的订单,管理员只能接“待接单”的订单,已经完成或者已取消的订单不能再次修改。我在 Service 层用了一个简单的状态判断:
java复制// 取消订单
if (order.getStatus() != 0) {
throw new RuntimeException("当前订单状态不可取消");
}
order.setStatus(4); // 4 表示已取消
orderMapper.updateById(order);
这种写法的好处是逻辑直白,答辩的时候也容易讲清楚。如果你想更专业一点,可以用枚举定义订单状态,把状态流转规则封装成 Map 或状态机工具类,但毕设场景下用一个静态方法或 if 判断足够了。
4. 前端页面与交互实现
4.1 JSP 页面模板与 Layui 的集成方式
前端部分我采用“JSP 负责数据渲染,Layui 负责交互组件”的分工方式。JSP 页面通过 ${} 表达式和 JSTL 的 <c:forEach> 标签来展示从 Controller 传过来的数据。例如首页展示服务分类,后端 model.addAttribute("categoryList", list) 后,页面里直接循环输出即可。
Layui 的引入方式,我建议把整个 Layui 目录放到 src/main/resources/static/layui/ 下,然后在 JSP 页面里通过 <link> 和 <script> 标签引用。不用 CDN 的原因很简单:部署环境不一定有公网,万一网络不通,整个页面的样式和交互就全崩了,本地引用最稳。
jsp复制<link rel="stylesheet" href="/static/layui/css/layui.css">
<script src="/static/layui/layui.js"></script>
使用 Layui 内置组件时,记得在页面里用 layui.use([...], function(){ ... }) 方式加载模块。比如加载表格模块用 layui.use('table', function(){ var table = layui.table; });。这个机制是百度内部大神的经典作品,采用模块化加载,指定的模块才能用,不声明会直接报错。
4.2 前台核心页面的实现要点
前台最重要的两个页面,一是服务列表页,二是订单提交页。服务列表页接收分类 ID 参数,后端根据分类查询服务项目并分页,页面用 Layui 的 laypage 组件渲染分页条。分页这里要注意:JSP 是服务端渲染,分页参数是通过 URL 传递的,所以点击页码直接跳转新的 URL 是最可靠的做法,而不是去走 Ajax 刷新。
订单提交页有几个关键字段需要处理:服务地址、联系人、联系电话、预约时间。这些字段的取值逻辑要严谨,比如电话在 Service 层做一下正则校验,预约时间通过日期选择器限制不能选过去的日期。我早期做的时候没限制过去日期,结果用户下单选了个昨天的日期,后台接单时才发现,非常尴尬。
个人中心的“我的订单”页面我用了一个状态筛选 Tab 组件,把待接单、已接单、服务中、已完成、已取消分别列出来。每个 Tab 点击后跳转到不同状态的订单列表 URL,后端根据参数查询。这个方案比 Ajax 无刷新切换更简单,也不容易出 Bug,用户体验也能接受。
4.3 后台管理页面布局与交互
后台管理端我完全使用 Layui 构建,包括顶部导航、左侧菜单、右侧内容区(iframe 方式)的经典布局。菜单项绑定 URL,点击后在 iframe 中加载对应的管理页面。这种方式实现成本低,不用写复杂的路由,而且管理系统的操作习惯上,用户已经非常熟悉这种布局。
服务项目管理页面是后台交互最复杂的部分。我用 Layui 的 table 模块渲染数据列表,操作列放了“编辑”和“上架/下架”两个按钮。点击编辑时弹出 layer.open 弹窗,里面嵌一个编辑表单页面,表单提交后通过 Ajax 请求更新数据,成功后刷新表格。
这里有一个自定义操作的细节:Layui table 操作列默认渲染 HTML 字符串,按钮事件需要用 table.on('tool(filter)', function(obj){}) 去监听。很多新手直接写 onclick 发现点击无效,就是因为没有正确监听 Layui 的工具条事件。把 lay-event 属性和回调事件绑定上,就能拿到当前行的数据,顺利执行后续操作。
5. 常见问题与排查技巧实录
5.1 JSP 页面访问 404 或白屏
这是整套项目里出现频率最高的问题,通常有三种原因。第一种是 pom.xml 里少了 tomcat-embed-jasper 依赖,JSP 文件根本没被解析,直接以文本形式返回或 404。解决方法是加上依赖后重新编译启动。第二种是 spring.mvc.view.prefix 和 suffix 没有配置,Controller 返回 "admin/service/list" 时,Spring Boot 不知道去哪里找 JSP 文件。
第三种情况比较隐蔽:打包方式写的是 jar 而不是 war。Spring Boot 的 jar 包运行方式对 JSP 支持不完整,平时用 IDEA 直接运行可能没问题,但打成 jar 部署就会出问题。建议直接设置成 war,然后用 spring-boot-maven-plugin 打包。
5.2 静态资源 404 但 JSP 页面正常
如果你发现 JSP 可以正常打开,但页面上 CSS、JS、图片全都加载不出来,这说明静态资源的访问路径或目录位置有问题。Spring Boot 默认会映射 classpath:/static/ 目录,所以 Layui 文件放在 resources/static/layui/,访问路径应该是 /layui/css/layui.css,注意 URL 里不需要带 static。
我遇到过一次比较特殊的情况:拦截器把静态资源拦截了。因为我的拦截器配置在 WebMvcConfigurer 里调用了 addInterceptors,但 excludePathPatterns 里写的是 /static/**,实际访问路径不带 static,于是全部被拦截跳转到登录页。后来改成排除 /layui/**、/css/**、/js/** 这些真实路径才解决。
5.3 Layui 表格初始化报错或数据不加载
Layui 的 table 模块加载数据显示空白,最常见的原因是请求的数据格式不符合要求。Layui table 默认要求后端返回的 JSON 包含 code、msg、count、data 这几个字段,其中 code 必须是 0 才算成功。如果你后端返回的是 {status: 200, result: [...]} 这种自定义格式,表格根本没数据。
解决办法有两个方向:一是在后端统一封装一个分页结果对象,字段设为 code、msg、count、data,直接用 layui.table.render 的 url 属性请求;二是在前端 parseData 回调里把后端数据格式转换成 Layui 期望的格式。我更推荐第一种,因为后端接口统一了,后续加新功能都方便。
另外一个容易踩的坑是 elem 参数指向的容器 ID 写错。页面里表格的 <table id="orderTable" lay-filter="orderTable"></table>,配置时 elem: '#orderTable',两个名称必须完全对应,写错一个就白屏或者报错找不到元素。
5.4 MySQL 连接失败和中文乱码排查
MySQL 连接报错 Access denied for user 时,先检查用户名密码是否配错,再看连接串里的 useSSL 和 serverTimezone 是否配置正确。MySQL 8.x 默认开启了 SSL,连接串里最好加一句 useSSL=false,避免控制台刷出大段 SSL 相关的警告日志。
中文乱码问题需要从四个环节排查:数据库表字符集是否为 utf8mb4、连接串是否带了 characterEncoding=utf8、JSP 文件头部是否声明了 pageEncoding="UTF-8"、后端代码中是否有转码处理。这四个环节只要有一个不对,中文就可能在某个环节变成问号。特别是 JSP 文件本身,<%@ page contentType="text/html;charset=UTF-8" language="java" %> 这行声明一定不能漏。
最后再分享几个实际操作中的体会
整套系统从零开始写到能跑通全部流程,大概花了两周多的时间。最花费精力的其实不是某个功能怎么写,而是各种细节问题的排查,比如 JSP 路径配错、Layui 模块没加载、数据库时区不对。这些单个看起来都是小问题,但串起来非常磨人。
我个人在做这类项目时有个习惯:先把数据库表设计好,再动手写代码。表结构一旦确定,实体类、Service、页面基本上都是顺着表走的,后面返工的概率会小很多。另外建议尽早把拦截器和 JSP 模板页面搭起来,后面每写一个功能就立刻放到模板里跑一下,别等全部写完再统一测试,那样光排查问题都够你熬几个通宵。
这套技术栈虽然相比现在的前后端分离方案显得朴素了一些,但它的价值在于链路短、逻辑直观、容易理解,非常适合用来完整走一遍 Web 项目的开发流程。如果你正在做类似的系统,希望这篇分享能帮你少踩几个坑。
