
最近好几个朋友都在问我 Spring Boot 能做点什么有实际业务价值的项目我第一个想到的就是家政保洁预约系统。原因很简单一方面这类型系统贴近真实业务场景从用户下单到后台派单、服务完成、评价结算整条链路清晰完整另一方面它足够撑起一个完整的 Spring Boot 全栈项目从 Web 开发的基础到权限管理、文件存储、定时任务都能覆盖到。这篇博客我把自己做过的一个家政保洁预约系统项目整理成完整的技术总结从需求拆解到数据库设计、核心模块实现、前后端打包部署再到实际踩过的坑全部梳理出来给准备做毕设或者想用 Spring Boot 练手的人一个可复用的参考。1. 业务需求分析与技术选型1.1 家政保洁预约系统到底要做哪些事在做任何系统之前先把业务看清楚。家政保洁预约系统不是一个简单的 CRUD 管理后台它涉及的角色和状态流转比表面看起来要复杂得多。从角色维度来看系统至少要支撑三类用户普通用户需要保洁服务的人、保洁员提供服务的劳动者、管理员平台的运营管理方。用户端的核心诉求是浏览服务项目日常保洁、深度保洁、家电清洗、开荒保洁等选择时间段下单预约然后跟踪订单状态直到服务完成并评价。保洁员端需要看到被分配的任务、接单、确认开始服务、确认完工。后台管理端则要维护服务项目信息、管理保洁人员档案、查看订单流转情况、处理用户评价内容。从订单状态这个角度看这里有一个非常关键的业务点状态机设计。一份预约订单从创建到完结至少要经过待支付、待指派、已指派待接单、进行中、待验收、已完成、已取消这几个状态。每个状态在什么条件下触发流转流转之后哪些角色能看见、能操作这是整个业务逻辑最核心的部分。我接触过的很多半成品项目问题恰恰出在这里——把订单状态直接做成一个 String 字段随手改没有统一的流转管理后面很容易出现状态错乱、订单卡死的问题。另外还需要考虑的一个业务点是预约时间冲突问题。家政保洁是按时间段维度的服务一个保洁员在同一个时间段内不能同时接两个订单。这种约束在数据库层面不好做更多是在服务层做校验这也是整个系统里比较考验逻辑严密性的一个点。1.2 技术选型的理由和取舍技术选型上我用的是一套非常经典的组合对中小型业务系统来说完全够用同时学习和维护成本都很低。后端框架选了 Spring Boot这一点应该没什么争议。Spring Boot 的核心价值在于简化了 Spring 应用的初始搭建和开发过程内嵌 Tomcat、自动配置、生态完善这些都是实实在在提升开发效率的。之所以强调 Spring Boot 而不是传统的 Spring MVC XML 配置组合是因为在快速迭代和团队协作场景下Spring Boot 的约定优于配置理念能大幅减少重复工作。持久层框架我用的是 MyBatis-Plus。很多人纠结 MyBatis 和 JPA 选哪个我的实际体会是如果你要完全掌控 SQL面对复杂的报表查询和动态条件拼接MyBatis-Plus 会更顺手。它内置了分页插件、代码生成器、通用 Service 封装单表 CRUD 几乎不用写 SQL复杂查询又可以通过自定义 XML 实现属于既节省时间又保有灵活性的选择。前端部分我用了 Vue ElementUI和 Spring Boot 通过 RESTful API 交互。选择 Vue 的核心原因有两个一是组件化开发模式很适合后台管理这类界面复用多的场景二是前后端分离开发时Vue 的生态完善从路由到状态管理到 HTTP 库都有成熟方案联调效率很高。MySQL 作为主数据库Redis 按需引入用于缓存和 token 存储。这里需要特别提醒一个点Spring Boot 2.x 和 3.x 在版本兼容上差异很大。Spring Boot 3.x 强制要求 JDK 17 及以上如果你本机环境还停留在 JDK 8直接用 Spring Boot 3.x 会直接报错。很多新手一上来就拉最新版本结果环境跑不起来这是我的第一个建议先确认 JDK 版本springboot 2.7.x 配 JDK 8 是当前兼容性最稳妥的搭配。1.3 数据库设计的核心思路数据库设计是整个项目的基石。我完成一个需求之后通常先用表格把核心数据模型理清楚然后再去建表而不是写代码写到一半临时加字段。系统里最核心的表至少有这几张用户表 user字段包括 id、用户名、密码加密存储、手机号、真实姓名、角色类型用户/保洁员/管理员、头像地址、创建时间。这里密码存储推荐 BCrypt 加密不要用 MD5这是很多成熟团队的基本要求。服务类别表 service_category 和服务项目表 service_item 是两级分类结构。比如服务类别是日常保洁服务项目是基础保洁2小时、深度保洁4小时等。每个服务项目包含名称、描述、价格、时长、图片地址、状态上架/下架。预约订单表 appointment_order 是重头戏。字段包括订单号这个建议用业务规则生成而非自增 id因为要对外展示、用户 id、服务项目 id、保洁员 id可空表示未指派、预约日期、开始时段、结束时段、地址、联系人、联系电话、订单状态、支付金额、支付方式、支付时间、创建时间、更新时间。索引设计上用户 id、状态、预约日期这三个字段是高频查询条件要建索引。评价表 review 关联订单和用户包含评分1-5星、文字评价、图片评价、创建时间。需要注意业务约束订单完成后才能评价一个订单只能评价一次。另外还有一张比较容易被忽略的表是保洁员排班或工时表可以记录保洁员某天可接单的时间段辅助派单。但考虑到不同公司精细程度不同这部分我建议初期可以简化接单冲突通过服务层校验来避免等业务量大了再考虑专用表。2. Spring Boot 项目结构搭建与核心机制2.1 项目标准化分层与创建流程我还是要强调一下 springboot 项目结构。这看起来是个基础问题但确实有大量项目没分层好后面越写越乱。我的标准分包方式如下controller 层只做参数接收、调用 service、返回统一响应service 层写业务逻辑mapper 层对应 MyBatis 的持久层接口entity 层放数据库实体dto 层放接口出入参对象vo 层放视图对象config 层放各类配置类common 层放统一返回结果、异常处理、工具类。一个容易被忽视的经验是实体类字段不直接暴露给前端controller 的出入参要单独定义 DTO/VO。这样做的好处是前端需要什么字段你就给什么字段不需要把一个包含密码加密串、逻辑删除标记的实体直接吐到接口里。比如订单列表接口的 VO 里可以把用户名称和项目名称冗余展示出来就不用前端再逐条去查。创建项目的方式我推荐直接用 Spring Initializr无论用 IDEA 自带的功能还是 start.spring.io 网页版都可以。勾选依赖时建议选择 Spring Web、MyBatis或 MyBatis-Plus 的 starter、MySQL Driver、Lombok、Validation。开启 Lombok 后实体类用 Data 注解就能自动生成 getter/setter/toString能省掉大量样板代码。启动类的写法上要注意 SpringBootApplication 组合注解包含的三个核心能力EnableAutoConfiguration 开启自动配置ComponentScan 扫描当前包及子包的组件SpringBootConfiguration 标识这是一个配置类。如果某个类不在启动类同级或子级包内默认扫描不到这是新手常见的注入失败原因之一。2.2 Spring Boot 自动装配原理与自定义配置了解 springboot 自动装配原理不单是应付面试对排查问题也很有帮助。Spring Boot 之所以能“零配置”启动一个 Web 应用是因为它在启动时通过 EnableAutoConfiguration 加载了 spring-boot-autoconfigure 包内 spring.factories 文件里声明的所有自动配置类。但这些自动配置类并不全部生效它们几乎都带有条件注解比如 ConditionalOnClassclasspath 里有对应的类才生效、ConditionalOnMissingBean容器里没有用户自定义的 Bean 才生效、ConditionalOnProperty配置文件中存在指定配置项才生效。举个例子你引入了 spring-boot-starter-webclasspath 里有了 DispatcherServlet 相关的类自动配置的 DispatcherServletAutoConfiguration 才会生效创建一个 DispatcherServlet 并注册到内嵌 Tomcat。理解这个原理有一个实际好处当你想覆盖 Spring Boot 默认行为时知道该怎么做。比如想自定义 JSON 序列化规则只需要定义一个 Jackson2ObjectMapperBuilderCustomizer 类型的 Bean自动配置的 ObjectMapper 在条件注解检测到已有自定义 Bean 时就会失效从而使用你的版本。我的经验是遇到“我明明配置了为什么没生效”这类问题第一反应应该是去查对应自动配置类上的条件注解和配置前缀而不是怀疑框架坏了。我在项目里还做了一个自定义 banner用在线生成器把项目名“家政保洁预约系统”转成 ASCII 字符图案放到 src/main/resources/banner.txt 里启动时终端会打印出来纯粹图个调试时的项目区分感同时它也是 Spring Boot 一个很轻量的扩展点想知道 banner 怎么生效的可以顺手看下实现源码。2.3 多环境配置与基础配置详解多环境配置是实际项目从一开始就该做好的一件事。我在项目里把配置拆成三个文件application.yml 作为公共配置application-dev.yml 开发环境配置application-prod.yml 生产环境配置。公共配置里放应用名、端口、Jackson 日期格式、文件上传大小限制等dev 环境配本地 MySQL 和 Redisprod 环境配云服务器数据库和缓存地址。激活环境的方式有两种一种是在 application.yml 里写 spring.profiles.activedev 来静态指定另一种是启动命令加参数java -jar app.jar --spring.profiles.activeprod。第二种在实际部署时更常用因为同一个 jar 包不用重新打包就能切换环境。关于端口配置IDEA 2026 里如果不知道怎么配置 Spring Boot 服务的启动端口可以直接在 application.yml 中修改 server.port 属性也可以在 Edit Configurations 里添加 Program arguments 写入 --server.port8081后者适合临时覆盖。启动端口修改后要注意前端联调的代理地址同步更新否则容易发生后端换了端口、前端还在请求旧端口导致 404 的乌龙。文件上传大小限制也需要主动配置Spring Boot 默认的单个文件上传上限只有 1MB这个在业务上完全不够用尤其是用户上传房屋现场照片、保洁前后对比图这些场景。我配置的是spring.servlet.multipart.max-file-size: 10MBmax-request-size: 20MB。如果项目中自定义了 MultipartResolver注意不要用 old 版本的 CommonsMultipartResolver直接用 StandardServletMultipartResolver 即可否则容易和 Spring Boot 的默认配置冲突导致上传参数不生效。3. 核心业务模块的落地实现3.1 用户认证与鉴权机制设计家政保洁预约系统不会只有游客可访问的公开接口用户下单、保洁员接单、管理员操作这些操作都必须登录后才能执行所以认证鉴权模块要在写具体业务之前就搭好。我采用的方案是 JWT 拦截器方式没有直接用 Spring Security。原因很简单业务场景没有复杂的角色权限层级管理后台的操作权限靠拦截器加手动判断就够了引入 Security 会带来大量配置成本对于这个体量的项目性价比不高。登录流程是这样的用户提交用户名和密码后端查询数据库并用 BCrypt 校验密码校验通过后生成 JWT Token在 Token 内存入用户 id、用户名、角色返回给前端。前端拿到 Token 后存在 LocalStorage 里每次请求都放到 Authorization 请求头中。后端写一个 LoginInterceptor 拦截所有非公开接口从请求头取出 Token解析并验证合法性把用户信息放入 ThreadLocal 供后续业务代码获取当前登录用户。拦截器注册方式是实现 WebMvcConfigurer 接口在 addInterceptors 方法中注册拦截器并配置 excludePathPatterns把登录接口、注册接口、首页展示的服务项目列表接口排除掉。Token 过期时间我设成了 24 小时家政预约场景用户不会频繁打开 App这个时效是合理的。登录接口不能明文传输至少用 HTTPS 密码 BCrypt 加密存储这是底线。注意JWT 无法在服务端主动失效。如果用户修改密码或管理员封禁账号此前签发的 Token 依然有效。应对策略是维护一份 Redis 黑名单封禁时把用户 id 写进去拦截器里查询。这个细节在答辩或评审时是加分项。3.2 预约订单状态机的完整实现订单状态流转是家政预约系统的核心业务逻辑我把它封装成一个独立的 OrderStateMachine 类避免业务代码里到处散落 if-else。状态枚举定义如下待支付、待指派、已指派、进行中、待验收、已完成、已取消。各状态流转规则集中在枚举方法里用户支付成功待支付转待指派管理员指派保洁员待指派转已指派保洁员开始服务已指派转进行中保洁员确认完工进行中转待验收用户确认验收待验收转已完成。有一个细节已取消状态比较特殊待支付状态下用户可以直接取消已指派状态下用户取消需要管理员确认或者设置超时自动取消。我在实现中额外加了一层数据库乐观锁防止并发操作导致状态错乱。更新订单状态的 SQL 语句格式如下int rows orderMapper.updateStatusByCondition( orderId, targetStatus, expectStatus, expectedUpdateTime );对应的 XML 里 update 语句会带上 and status #{expectStatus} 这个条件。rows 等于 0 表示状态已被其他请求修改事务回滚并向调用方抛出业务异常提示“订单状态已更新请刷新后重试”。这种做法虽然简单但确实能兜住绝大多数并发场景的问题。业务校验上还有一个容易被忽略的点保洁员接单冲突。同一保洁员在同一个时间段只能有一个进行中的订单。实现方式是在接单/派单前查询数据库确认该保洁员在目标时间段没有冲突订单。这种校验在并发量不大的前提下是足够的。3.3 定时任务处理订单超时与到期提醒springboot 定时任务是订单系统里面一定要有的功能。在真实业务里用户经常下单后忘了支付或者服务完成后忘了去验收如果没有定时任务驱动超时处理大量脏数据堆积会影响后续派单和结算。我的做法是在启动类上加 EnableScheduling 开启调度功能然后定义两个定时任务方法并用 Scheduled 注解标注。第一个任务每 5 分钟执行一次扫描创建时间超过 30 分钟且状态为待支付的订单自动将其置为已取消状态并释放对应的保洁员时间段资源。第二个任务每小时执行一次扫描服务时间在次日且状态为已指派的订单给用户和保洁员发送站内信或短信提醒。这里我没有引入消息队列因为定时任务在这个场景已经够用换成消息队列反而增加部署复杂度。实操中有一个坑要提醒Scheduled 默认是单线程执行如果一个任务卡住后续任务都会排队等待。处理方式是自定义一个线程池任务调度器将任务分发到独立线程执行Bean(name taskScheduler) public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix(scheduled-task-); return scheduler; }为什么要强调这一点我在调试阶段遇到过一个问题一个批量扫描任务因为数据库连接超时卡了十分钟结果两个定时任务串行等待订单超时处理全部延后。换成多线程调度器后单个任务异常就不再影响其他任务了这个经验在真实生产环境非常有用。3.4 文件上传与 MinIO 整合家政保洁系统涉及到的图片上传场景不少用户上传房屋照片、上传评价图片、管理员上传服务项目展示图。方案上我没有把图片存到数据库或服务器本地磁盘而是整合了 MinIO。MinIO 是一个开源的对象存储服务兼容 Amazon S3 API可以很方便地私有化部署。为什么选它而不是阿里的 OSS 或者腾讯的 COS因为它的部署成本几乎为零在本地 Docker 一条命令就能跑起来而且 API 和云厂商兼容以后迁移到云上代价很小特别适合毕设和中小型项目。Spring Boot 整合 MinIO 的步骤不复杂引入 minio 的 Java SDK 依赖在 application.yml 里配置 endpoint、accessKey、secretKey、bucketName然后创建一个 MinioConfig 类实例化 MinioClient 并注册为 Bean。上传接口接收 MultipartFile生成一个带时间戳和随机串的文件名调用 MinioClient 的 putObject 方法上传最后返回可访问的文件 URL。这里有几个细节值得注意。第一文件名一定要处理不能直接用用户上传的原始文件名。一方面可能有非法字符另一方面如果两个用户上传了同名文件会互相覆盖。我的做法是用“日期/随机UUID/原始扩展名”的格式重新生成文件名。第二bucket 的访问策略要配置好后才能通过 URL 直接访问文件。第三前端上传组件超时时间要放宽上传大图时如果前端默认最大超时时间太短图片传到一半就断了而且不会报错接口层容易产生半截垃圾文件。4. 前后端联调与打包部署的关键细节4.1 跨域配置与接口联调实战前后端分离开发时最烦的一个问题是跨域。Vue 开发服务器默认跑在 5173 端口Spring Boot 后端跑在 8080 端口前端直接请求后端接口会因为浏览器的同源策略被拦截。我的处理方案分成开发环境和生产环境两个维度。开发环境采用 Vue CLI 或 Vite 的 proxy 代理配置把 /api 前缀的请求代理到 http://localhost:8080浏览器看到的请求是同源的不受跨域限制。生产环境则不同打包后的 Vue 静态资源放在 Spring Boot 的 static 目录下前后端同源部署跨域问题自然消失。但即使同源部署开发时也不能完全不处理跨域因为如果不小心用 IP 地址直接访问后端接口还是会遇到跨域问题。稳妥的做法是在后端配置一个全局跨域过滤器允许指定的前端源访问Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里要注意 allowedOrigins 和 allowedOriginPatterns 的区别。如果配置了 allowCredentials(true) 并且使用 * 通配符在 Spring Boot 2.4 以上版本必须用 allowedOriginPatterns(*)否则启动时会直接抛出 IllegalArgumentException这也是一个版本升级后常见的坑。4.2 Vue 构建产物整合进 Spring Boot 的打包方案在部署层面我选择了将 vue 打包产物放进 springboot 中的方式而不是单独部署 Nginx这样整个系统就是一个单一的 jar 包部署时只需要 Java 运行环境不需要再装 Node.js、Nginx 等额外组件。对于毕业设计演示和中小型项目来说这种部署方式最省事。具体做法分三步。第一步在 Vue 项目的 vite.config.js 中设置 base 为相对路径 ./避免打包后资源路径以绝对根路径开头导致嵌套部署时资源 404。第二步执行 npm run build产物生成到 dist 目录把 dist 目录下的所有文件复制到 Spring Boot 的 src/main/resources/static 目录下。第三步重新打包 Spring Boot 工程访问 http://localhost:8080/ 就能直接看到前端页面。但有一个坑必须提一下如果你选择把前端打包结果放进 Spring Boot 的 static 目录那么 SPA 的路由模式一定不能用 history 模式要改成 hash 模式。原因在于 history 模式刷新某个具体路由时后端没有对应的映射返回 404 页面。hash 模式是通过 URL 中 # 后面的路径做路由匹配这部分不会发送到服务器因此刷新能正常工作。在配置路由时const router new VueRouter({ mode: hash, routes });如果你坚持想要 history 模式带来的干净 URL那必须在后端加一个 controller把非接口请求全部 forward 到 index.html。但我个人建议项目阶段不必折腾hash 模式完全够用。4.3 Maven 多模块构建与依赖管理项目如果是单模块工程打包部署是最简单的mvn clean package 打出可执行 jar 就行。但如果代码规模增大或者你想按模块拆分比如 common、system、business 三个模块就建议用 Maven 多模块结构。多模块构建的核心是父 pom 的 dependencyManagement 加子模块的 dependencies。父 pom 里用 spring-boot-dependencies 作为 BOM 统一管理版本子模块再按需引入具体依赖不需要写版本号。需要注意 spring-boot-maven-plugin 要配置在需要打成可执行 jar 的模块中而且要用 repackage 的 execution 配置否则打出来的 jar 内部不包含嵌入的 Tomcat就没办法 java -jar 直接启动。SSM 时代的项目通常是把所有代码放在一个 webapp 里然后打成 war 包丢到 Tomcat 的 webapps 目录下。Spring Boot 下的标准做法是打成 fat jar内部内嵌 Tomcat启动方式变成 java -jar xxx.jar。如果你在本地跑得好好的但服务器上启动报 “no main manifest attribute”多半是 spring-boot-maven-plugin 没生效或没执行 repackage这个排查点先记住。5. 常见问题排查与性能优化建议5.1 Spring Boot 启动与运行高频问题速查我把实际开发中遇到和答疑中比较常见的高频问题整理成了一张速查表按关键词分类方便遇到问题时快速定位。问题现象根因分析解决方案启动报端口被占用上次进程未关闭或其他服务占用 8080找到占用进程并杀掉或修改 server.port启动报 Cannot determine embedded database driver class引入了 JPA 但未配置数据源检查 MySQL 连接地址、账号密码是否正确或排除无关依赖启动报 Failed to configure a DataSource数据源配置缺失在 application.yml 补全 spring.datasource 配置Service 注入为 nullNullPointerException启动类扫描不到 bean检查包路径确保 Service 在启动类同级或子级包下MyBatis 映射器方法报 Invalid bound statementmapper 接口和 XML 未绑定在 application.yml 配置 mapper-locations 指向 mapper XML 路径上传文件失败提示文件大小超限未配置 multipart 限制配置 max-file-size 和 max-request-size前后端联调请求 404接口路径写错或请求未到达后端检查 Controller 的 RequestMapping 和前端请求路径前缀刷新前端路由 404前端使用 history 路由模式改为 hash 模式或后端配置 forward 到 index.html定时任务不生效启动类未加 EnableScheduling补上 EnableScheduling 注解或检查 Scheduled 方法是否被调用这里面最值得多讲一句的是 MyBatis 映射文件扫描不到的问题它的案例很典型。默认情况下 Spring Boot 只会加载 classpath 下的资源文件如果你把 mapper XML 放在 src/main/java 的包目录下Maven 在编译时默认不会把 .xml 文件复制到 classes 目录运行时自然会报 Invalid bound statement 错误。解决方案有两种一是把 XML 文件放 src/main/resources/mapper 目录二是如果坚持放包目录就需要在 pom.xml 里配置 resources 资源打包规则。resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources这个配置很容易遗漏但它确实很多项目报错的关键遇到映射器找不到方法的问题优先检查这个。5.2 查询性能优化与缓存应用当业务代码跑通之后性能优化就变成下一个重点。家政预约系统在数据量达到一定程度后最可能出现的问题是首页服务项目查询和订单列表查询较慢。我的优化思路分成三层。第一层是 SQL 层面排查慢查询给高频条件字段加索引。第二层是缓存层面把热门服务项目列表缓存到 Redis。服务项目数据是典型的读多写少场景缓存命中率会很高。实现方式是在查询接口里先查缓存缓存未命中再查数据库并回填缓存缓存过期时间设为 30 分钟。后台修改项目信息时主动删除对应缓存键保证数据变更后能及时生效。第三层是分页查询优化用 MyBatis-Plus 自带的分页插件注意大偏移量场景下使用子查询或游标分页避免 LIMIT 100000, 10 这种性能较差的 SQL。性能优化不是一味堆配置先去日志里看真实耗时的 SQL再针对性优化这个原则我一直相信。很多项目一上来就加 Redis、加 MQ看起来“高级”但实际上乱配缓存反而可能引发缓存穿透、缓存雪崩等问题比不加缓存还糟糕。我建议先把 SQL 索引建好再逐步叠加缓存。5.3 项目扩展方向的可能性家政保洁预约系统做完核心链路之后后续扩展方向还有不少可以探讨的。比如引入消息队列来解耦订单超时处理和通知推送把定时任务轮询改成延迟消息触发系统响应会更及时。再比如接入支付平台微信支付或支付宝的 SDK 集成订单支付环节就能从模拟支付变成真实支付。地理位置服务也是一大块用户下单时自动定位地址保洁员端显示路线导航这能让系统在真实业务中更好用。如果技术上想更进一步可以把大模型能力接入进来比如做一个基于历史订单的智能推荐模块为用户推荐最适合的保洁服务包。这些都是在不改变现有架构基础上就能逐步扩展的方向不会推倒重来。写在最后的个人体会这个项目做完之后我最大的体会是家政保洁预约系统的核心不在页面多好看而在于订单状态流转的严谨性把状态图理清楚、把并发控制做好后面所有模块的开发都会顺畅很多。Spring Boot 在这里的作用是把你从繁琐的环境配置中解放出来让你把精力真正放到业务上。如果你正在用这个项目练手我建议按“先搭框架、再理数据、然后实现订单链路、最后补全权限和部署”的顺序推进每一步走稳整个项目就不会有大的返工。遇到问题时优先看日志和官方文档这是解决 Spring Boot 相关问题最有效的方式。