ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue社区家政预约平台:从状态机设计到上线交付全解析

SpringBoot+Vue社区家政预约平台:从状态机设计到上线交付全解析

前阵子陆续有几位读者来问同一个需求:想做一个社区家政服务预约平台,技术栈限定springboot+vue,还要带源码、文档,最好能帮忙调试、改改基础功能、答疑。我一开始以为只是随便问问,后来发现这个问题背后其实是完整的一类项目诉求——既要能通过课程设计或毕业设计的验收,又要真正跑起来给小区物业、中小家政公司内部试用。顺手把我的经验整理成一篇,从需求设计、数据库、后端、前端到联调交付,尽量说全说透,希望能帮你省下几周摸坑的时间。

1. 为什么社区家政预约平台选择了springboot+vue这条技术路线

社区家政服务的典型场景大家都不陌生:保洁、家电清洗、保姆月嫂、管道维修这类服务,过去靠电话预约或者微信群接龙,漏单、撞单、爽约经常发生,想要查某个订单的历史记录,比翻聊天记录还难。一个预约平台要做的就是三件事:业主能在线看服务、选时间下单,家政人员能接单和确认完成,平台方能审核上架和做基本的数据统计。

技术选型上,springboot+vue几乎是最稳妥的组合,甚至可以说是这类管理型Web项目的默认答案。

先说后端。SpringBoot把配置简化到了极致,一个启动类加一个application.yml就能跑起来,内嵌Tomcat连部署都不用额外装。配合MyBatis-Plus这类国产增强框架,单表增删改查基本不用写SQL,代码量可以压到很低。对大多数学生项目来说,不需要上微服务、消息队列这些重型中间件,SpringBoot单体能扛住几百人的小区使用量,也足够答辩演示。

再说前端。Vue的数据绑定和组件化思想,对新手来说上手门槛远低于React。配合Element Plus组件库,表单、表格、弹窗、时间选择器这些后台管理页面的常规组件全都现成,一个上午就能把页面框架搭出来。加上Vue生态的成长路线很平滑,后面想升级做小程序端、或改成移动端H5,核心逻辑都能复用。

还有一个很现实的原因:遇到问题时能搜到的资料率很高。SpringBoot的接口如何写、JWT怎么集成、Element Plus怎么用、Vue路由守卫怎么写,几乎每个坑都有人踩过、有人写博客讲清楚。对一个以完成项目为主要目标的开发者来说,这比任何技术先进性都重要。实际上,这几年我接触到的家政类、预约类、社区服务类项目,超过一半都采用了这套组合,跑通率也最高。选它不一定最惊艳,但一定最不容易翻车。

2. 业务闭环先于代码:订单状态机的设计决定了系统上限

很多开发者拿到项目需求,第一件事就是打开数据库建表,恨不得一个中午把表建完。但如果家政预约平台也这么干,后面大概率要做大量返工。原因很简单:这类系统的核心不是服务分类的增删改查,而是订单从创建到完成的整个生命周期。生命周期里的状态怎么流转、每个状态下谁能做什么操作、不允许跳转哪些状态,这些不做清楚,接口写出来一定前后矛盾。

理清业务闭环,我建议先守住三张角色图和一条主流程线。

三个角色分别是:客户(下单的人)、家政人员(接单干活的人)、管理员(平台运营的人)。客户的操作路径是:注册登录、浏览服务分类、查看服务详情、选择预约日期时间、填地址与联系人、提交订单、等待确认、服务完成后评价。家政人员的路径是:登录后查看被分配或者被点单的预约、确认接单、按时上门、服务结束后确认完成。管理员路径是:审核服务项目是否上架、审核家政人员入驻资质、查看订单列表处理异常单、查看基础数据看板。

主流程线就是一条订单状态机。我实际项目里的状态取值是这样的:

状态值名称触发动作
0待确认客户提交预约订单
1已确认家政人员接单或管理员后台确认
2服务中家政人员点击开始服务
3已完成家政人员点击完成服务
4已取消客户取消或管理员取消
5售后中客户发起投诉或退款申请

状态机的核心原则是:每次状态变更只能从当前状态跳转到合法状态,不允许跳级,不允许逆向(特殊情况除外,比如管理员强制取消)。例如,待确认状态可以由客户自己取消变成已取消,但一旦家政人员接单变成已确认,就不能静默取消,必须走退款或售后流程。这个规则在代码里必须显式定义,而不是靠每个接口各自做一个if判断。

为方便讲解,订单状态机的设计值得前置到建表之前。先画状态转换表,再对着表写接口,逻辑会清晰很多。通常我建一个常量类专门管理状态值,不让魔法数字散落在业务代码里。

3. 后端落地细节:分层结构、登录鉴权与统一返回格式

3.1 三层结构怎么拆才清晰

后端代码结构不必花哨,但一定要分层明确。我习惯拆成这样:

  • controller:只接收参数、调用服务、返回统一结果,不在里面写业务逻辑
  • service:负责业务逻辑,比如下单校验订单状态、校验服务时间冲突
  • mapper:继承BaseMapper,负责数据库交互
  • entity:与表字段一一对应的实体类
  • dto / vo:入参对象和出参对象,避免把实体类直接暴露给前端

有人图省事,直接在controller里写一堆业务代码,项目小的时候还能跑,但只要加上"预约时间是否可选""工人当天是否已经被约满"这些判断,controller会迅速膨胀到几百行。分层的好处不是代码好看,是后续排查问题和改需求时能快速定位。比如面试官或老师问"订单状态这个逻辑在哪里控制",你能直接说出service.order.status里的checkAndUpdate方法,这就是说服力。

3.2 JWT登录和角色权限用一套账号表顶三代

家政平台涉及三种角色的登录。很多新手会建三张表:user表、worker表、admin表,然后分别写三套登录接口。这个设计维护成本很高,而且三种角色其实共享账号密码、手机号、头像这些基础字段。我更推荐一张account账号表,加一个role字段区分角色,值分别为USER、WORKER、ADMIN。家政人员或管理员本质上都是account,扩展一个worker_info表存他们的专属资料即可。

登录接口返回JWT令牌,前端把令牌存localStorage,每次请求带上Authorization头。后端写一个登录拦截器,对需要登录的接口校验令牌,并从令牌中解析出用户id和角色。这里提个建议:如果项目没有强制要求,不必上Spring Security。它虽然功能全,但概念多、配置重,对这类系统属于杀鸡用牛刀。用拦截器配合角色判断,代码简单、运行效率高,讲起原理也更容易自圆其说。

3.3 统一返回与全局异常,别让报错暴露给用户

前后端联调时最怕的就是接口返回结构不统一。有的接口返回true,有的返回"操作成功",前端取数据时要针对每个接口单独处理,极容易出bug。建议定义统一返回类Result:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }

同时写一个全局异常处理器,把参数校验异常、业务异常、兜底异常统一转成Result返回,这样即使代码某个位置漏了处理,前端也只会看到规范化的JSON,而不是一长串堆栈信息。配合自定义异常类BizException,在service里随时抛出业务错误,比如"该时间段已被预约",前端直接弹message提示即可。

后端接口里,订单相关的操作我建议刻意取语义清晰的动作词:createOrder对应下单,confirmOrder对应接单,cancelOrder对应取消,completeOrder对应完成。动词式接口配合状态机,整个调用链会非常直观。

4. 数据库表这样设计,后续改需求才不会推倒重来

4.1 七张核心表的职责划分

家政预约平台不算复杂,但至少需要七张核心业务表。一张表对应一个清晰领域概念,这能让后期改造的维护成本大幅降低。

  • account表:账号公共信息,username、password、phone、role、nickname、avatar、status
  • address表:客户的收货/服务地址,receiverName、receiverPhone、省市区、详细地址、是否默认
  • service_category表:服务分类,如保洁、维修、月嫂
  • service_item表:服务项目,挂接分类,包含名称、价格、计价单位、预计耗时、封面图
  • worker_info表:家政人员资料,关联account,包含真名、电话、可服务项目、接单状态、审核状态
  • order_info表:预约订单主表
  • comment表:服务评价,关联订单、用户和家政人员

分开来看只是普通的“增删改查表”,但合理拆分的价值在于:新增一个服务维度字段,只要改service_item表,不影响订单;家政人员入驻审核功能,只需扩展worker_info字段;未来想给不同服务加不同计价规则,也只要在service_item里加字段。表职责清晰,意味着需求变更的冲击被隔离在局部,而不是一路波及所有表。

4.2 订单表字段设计的几个关键决策

order_info是整个系统的心脏,它的设计质量直接决定开发体验。核心字段我建议这样安排:

id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '自增ID', order_no VARCHAR(32) UNIQUE COMMENT '订单号,给用户看的编号', user_id BIGINT COMMENT '下单用户ID', worker_id BIGINT COMMENT '接单家政人员ID', service_item_id BIGINT COMMENT '服务项目ID', address_id BIGINT COMMENT '服务地址ID', service_time_start DATETIME COMMENT '预约开始时间', service_time_end DATETIME COMMENT '预约结束时间', status TINYINT COMMENT '订单状态 0待确认 1已确认 2服务中 3已完成 4已取消 5售后中', order_amount DECIMAL(10,2) COMMENT '订单金额', pay_type TINYINT COMMENT '支付方式', remark VARCHAR(255) COMMENT '客户备注', create_time DATETIME COMMENT '创建时间', pay_time DATETIME COMMENT '支付时间'

这里有几个决策值得展开。

第一,为什么订单号order_no要单独存在,而不是直接用自增id?因为用户和后台沟通时会说"我那个订单号是多少",自增id位数少且连续,容易被试探和猜测,也不利于分段检索。订单号通常用时间戳加随机数生成,长度16到20位之间即可。

第二,为什么预留service_time_end而不是只存service_time_start?因为家政服务普遍按时长计费。保洁按小时、维修按上门时长,预留结束时间后,订单列表页可以展示"预计服务时长",统计家政人员每天的总工作量也要用到这个值。哪怕MVP阶段用不到,也应该先占住这个位置,后面补上比改表结构容易得多。

第三,物理外键要不要加?我的建议是不加。数据库级的外键约束,删除分类或服务时会受到各种限制,维护成本高。业务层面通过service_item_id关联,在service代码里做存在性校验即可。这个做法在真实企业项目里很常见,面试时也能讲出理由。

第四,关于时间的坑。这类系统任何时间字段都要统一用datetime或者timestamp,且后端接收前端传来的时间参数时,一定要用@JsonFormat标注格式,或者全局配置Jackson序列化规则,否则前端传"2025-06-01 09:00:00",后端解析失败直接报400错误。我甚至遇到过前后端显示时区不一致,数据库存的时间比实际早了8小时的情况,排查到最后发现是MySQL时区配置问题。这些内容在后端配置阶段就应该处理掉,而不是等到联调再救火。

5. 前端从0到能点通全流程:路由、组件与预约表单的体验

5.1 前后台同工程:路由和布局

家政平台的前端,我建议把用户前台和管理员后台放在同一个Vue工程里。很多教程喜欢拆成两个前端项目,说是职责分离,但实际上两人份的登录态维护、两套打包配置、两处部署,对单人开发是负担。用路由前缀区分即可:普通页面放在 / 下面,后台管理页面统一挂在 /admin 下。

路由层面简单的划分:

  • 公共页面:登录页、注册页、首页、服务列表、服务详情
  • 用户页面:预约下单、订单列表、订单详情、服务地址管理、个人中心
  • 管理页面:数据看板、订单管理、服务分类管理、服务项目管理、家政人员管理、评价管理

这样划分后,权限控制也方便:普通用户登录后能访问用户页面,管理员进入后台,路由守卫按角色过滤。

5.2 登录态与权限:路由守卫必须做

前端路由守卫是这个系统不能省的部分。用Vue Router的beforeEach钩子做统一拦截,逻辑也不复杂:判断目标路由是否需要登录,需要则检查本地有token没有;有token还要判断访问的是不是管理员页面,是则校验角色是否ADMIN;不满足条件就跳回登录页。

有人觉得后台的某些页面反正接口也会校验权限,前端路由守卫是多余的。实际上不是这样。前端守卫的价值在于体验:未登录用户点击"我的订单"时能及时被带到登录页,而不是等接口报401再弹错误。权限错误和未登录错误的提示文案也应该区分,否则用户完全不知道为什么点按钮没反应。

5.3 预约表单的体验细节:时间选择、防重复提交

预约下单页是整个前端交互的难点,问题集中在三个地方。

第一个是时间选择。Element Plus的date-picker和time-picker支持禁用过去时间,设置disabled-date为今天之前的日期不可选即可。如果业务上要求同一天内上午的时间也禁掉,可以用time-picker的disabled-hours,把当前时分之前的时间禁用。这块很容易被忽略,等测试人员发现"为什么能预约昨天的时间"时再改,又要多一轮联调。

第二个是地址联动。选择省市区用级联选择器,最后一行填详细地址。地址保存后要支持设为默认地址,这个字段在account里用is_default标记,下单页默认选中默认地址即可。注意切换地址时表单校验要更新,防止用户改了地址但提交时仍是旧数据。

第三个是防重复提交。用户点击"提交预约"后,如果接口响应慢,他会习惯性再点一次,导致同一个订单创建两份。前端解决方案是:提交后按钮立刻loading并禁用,等待接口返回后再恢复。但这只是体验层面的兜底,真正的安全防线在后端的防重复创建逻辑,这一点下一章详细讲。

用户端预约成功后的引导也很重要。下单成功后不要直接清空页面,而是跳转到一个支付成功或下单成功提示页,并显示"查看订单"按钮,让用户看到下一步的等待确认状态。这能大幅减少"我的订单到底提交了没有"这类反馈。

6. 预约冲突与订单状态流转:联调时最容易翻车的地方

6.1 并发预约冲突:数据库唯一索引 vs 事务查询

下面聊两个真正会在联调阶段炸掉的点。

第一个是预约冲突。场景是这样:用户A预约了某保洁阿姨周六上午9点到11点,用户B在同一时间也要约同一个阿姨。如果后端只做普通的查询,发现9点到11点没记录就允许下单,在高并发或用户双击的场景下,可能两个请求同时查到"空闲",然后同时插入成功,阿姨就被约重了。

网上常见的防御手段有两种。第一种是前端把时间按钮置灰,看起来能解决问题,但同一个用户开两个浏览器标签页、或者手速够快连点两次,照样绕过去。第二种是数据库加唯一索引,比如对(worker_id, service_time_start)加唯一约束,能挡住同一开始时间的重复预约,但如果用户A约9点到11点,用户B约10点到12点,两个记录的开始时间不同,唯一索引也拦不住。

真正可靠的做法是用事务加锁。在创建订单时,开一个事务,先查该家政人员在目标时间段是否存在重叠订单:

@Transactional public Long createOrder(OrderCreateDTO dto) { // 加行锁或区间锁防止并发 Long conflictId = orderMapper.selectConflictOrder(workerId, startTime, endTime); if (conflictId != null) { throw new BizException("该家政人员在此时段已被预约,请更换时间"); } // 插入订单 ... }

对应SQL查询重叠订单的逻辑是:

SELECT id FROM order_info WHERE worker_id = #{workerId} AND status IN (0, 1, 2) AND service_time_start < #{endTime} AND service_time_end > #{startTime} LIMIT 1

这个条件判断的是时间区间是否有交集,覆盖率比单纯查startTime准确得多。配合数据库里的worker_id和service_time_start唯一索引,双保险下来基本可以杜绝撞单。对课程设计而言,事务加重叠查询已经十分够用,不需要上分布式锁。

6.2 状态转换不写保护,后台一定会把已取消订单接单

第二个坑是状态转换的原子性。我见过不少代码这么写:接单接口先查订单,拿到订单后判断status是不是0,如果是0就update成1。问题在于,网络延迟、用户取消请求先到、管理员并发操作时,两步操作之间状态可能已经被改了,但第二步拿着旧数据照样更新成功。

要避免这个问题,最好的办法是把校验和更新合成一条带状态条件的SQL:

int rows = orderMapper.updateStatusIfMatch(orderId, targetStatus, expectStatus); if (rows == 0) { throw new BizException("订单状态已变化,请刷新后重试"); }

对应的更新SQL:

UPDATE order_info SET status = #{targetStatus}, update_time = NOW() WHERE id = #{orderId} AND status = #{expectStatus}

这里影响行数rows为0,说明状态不是预期状态,直接抛异常让前端刷新。这一招在电商、库存类系统里是常客,叫作乐观锁。好处是简单、无额外依赖、天然防并发。像取消订单这些操作也一样,统一走带状态校验的更新方法,订单状态就不会被用户反复横跳搞乱。

如果服务层能保证所有状态变更都收敛到同一个方法里,比如changeOrderStatus(orderId, expectedStatus, targetStatus),后面加一个"待支付"状态或者"退款中"状态,都只要在这个方法里补充规则,而不是满项目找散落的update语句,这也是前面说状态机设计重要的落地体现。

7. 从本机跑通到交付验收:源码交付阶段的调试、修改与答疑

7.1 本机把项目跑起来的顺序

无论你是学这套项目还是准备交付给他人,先保证自己能按干净步骤跑起来。顺序一般是这样:

  1. 新建数据库,导入项目根目录下sql文件,确认初始化数据存在
  2. 修改后端application.yml里的数据源配置,数据库名、用户名、密码按本地环境调整
  3. 启动后端,确认控制台无报错,接口文档页或测试接口能通
  4. 前端目录执行npm install,然后npm run dev,等待页面自动打开
  5. 用初始化给的默认账号登录,跑一遍下单、接单、完成的完整流程

强调一个容易被卡住的细节:很多项目的sql文件是有人用MySQL旧版本导出的,字符集可能是utf8mb4也可能不是。导入之前先确认排序规则,如果是utf8mb4_general_ci,统一在数据库配置里也设置成utf8mb4,否则中文数据可能显示乱码。另外前后端数字端口要分开,后端常见8080,前端Vite开发端口常是5173或8081,不要让两个端口撞在一起。

7.2 跨域问题的三种处理方式

前后端分离必然遇到跨域。报错信息通常是Access-Control-Allow-Origin之类,新手一看见就慌,其实就三类解法。

第一种在后端写CORS配置类,允许指定来源跨域。这种方式代码最少,但如果前端端口改了,要回头改后端配置,稍微麻烦。第二种在前端开发服务器配代理,比如Vue CLI的vue.config.js里配置devServer.proxy,把/api转发到后端地址。这种方式最推荐在开发阶段使用,前端代码里所有请求都走相对路径/api/xxx,跟部署后的nginx代理行为一致,迁移到生产环境时前端代码一行不用改。第三种在上线后用nginx反向代理,把 / 指向前端dist静态文件,/api/ 转发到后端jar的端口,彻底进站同一域名,从根上消灭跨域。

实际开发时我一般开发用第二种、上线用第三种。后端不写跨域代码,框架层面干干净净。

7.3 文档、基础修改与答疑的边界

交付阶段的"文档"和"答疑",比很多人想的重要。落地说就是三份东西:数据库脚本、环境启动说明、功能清单表。环境说明里至少写清JDK版本、Maven或Gradle版本、Node版本、MySQL版本,这些不一致是跑不起来的头号原因。功能清单表按角色列清楚每个页面和接口的对应关系,验收时照着表格走一遍,双方都省心。

所谓"基础修改",在装修服务预约平台里通常指:换logo和站点名称、改首页轮播图、调整服务项目价格、修改联系方式。这些改动的共同特点是不涉及表结构变化。做这类改动时,建议把静态资源统一放在assets目录,文案尽量集中在配置项里,而不是散落在几十个vue文件内部。我甚至会把站点名称做成一个公共常量文件,改一行全局生效,答疑时只需要告诉对方"这个文件第二行就是站点名"。

"答疑"阶段最常被问的技术问题,其实大半都不是业务代码的问题,而是环境问题:端口占用、npm install失败、MySQL密码加密导致连接失败、Node版本过高导致依赖不兼容。准备一份常见环境问题排查清单,比反复远程看屏幕要高效得多。Node版本过高导致安装依赖失败这个尤其常见,遇到时优先建议降级到16或18版本再试,能解决七成相关报错。

8. 接手一套源码时,最快读懂项目的方式

最后分享一个我自己屡试不爽的经验:拿到一套陌生源码,不要从头一行一行读代码。那是图书馆式阅读法,效率极低。正确顺序是:先把项目跑起来,注册账号走一遍完整业务流程,对系统能做什么建立体感;然后打开数据库看表,对着订单状态流转图理解数据怎么变化;最后再回到代码里,重点看service层的事务和状态更新逻辑。三步下来,你基本就知道改哪个文件能改什么功能了。

这套方式对家政预约平台的源码同样适用。如果要改"预约时间粒度从小时变成半小时",先改前端时间选择器的format和step,再改后端的service_time字段校验,最后检查重叠查询逻辑里的时间比较方式,三处改完就对齐了。很多人觉得"基础修改"很难,其实难在不敢动、没有定位问题的思路,而不是改动本身。

我实际操作中还有个小习惯:每拿到一个系统,先在本地开一个分支或者备份一份sql,任何改动前都能一键还原。对待交付项目,这能避免改坏了又不好意思问原作者的尴尬。希望这篇能把家政预约平台的骨架和容易踩的坑讲清楚,接下来你可以动手打开代码了,跑通第一个订单的时候会特别有成就感。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进