
家政服务平台前后端分离实战复盘SpringBootVue3MyBatis这套组合怎么落地最省心之前在做一个家政服务类的接单平台需求倒不复杂但涉及的角色、状态流转、订单拆分、结算逻辑这些叠在一起如果一上来就乱写后期维护绝对想骂人。我选型定的是SpringBoot Vue3 MyBatis MySQL前后端完全分离。现在项目已经跑起来订单、阿姨管理、服务类目、评价、结算这些核心模块都正常运转把整个落地过程中的设计思路、关键代码、踩过的坑一次性整理出来希望能帮到准备做类似管理系统的同学。这套系统适合谁参考正在做毕业设计的学生、准备接外包的独立开发者、或者公司内部要做服务预约类平台的技术同学都可以直接对照这篇文章的思路去搭自己的模块。我不会只贴代码会把“为什么这么设计”也讲清楚毕竟光会复制粘贴换个需求你就懵了。1. 需求分析与系统设计思路1.1 家政服务平台的业务场景拆解家政服务平台表面上看就是“用户下单—平台派单—阿姨接单—服务完成—结算评价”这条线但真做起来里面的角色权限和状态流转比想象中复杂。我梳理下来至少涉及三种角色C端用户下单的人、服务阿姨/技师接单干活的人、平台管理员审核、派单、处理投诉、结算。有些平台还会拆出“门店/站长”这个中间层但作为第一版迭代我建议别贪多先把三端角色做扎实。核心业务链路是这样的用户浏览服务类目保洁、月嫂、做饭、家电清洗等选择具体的服务项填写地址和预约时间。系统生成订单初始状态为“待支付”用户支付后变成“待派单”。管理员在后台看到待派单列表手动或者按规则派给合适的阿姨。阿姨在接单端看到派给自己的订单确认接单后状态变成“待服务”。上门服务完成后双方确认状态变为“待评价”。用户评价阿姨也可以回评订单完结进入结算流程。这条链路里最容易被忽视的是取消订单、退款、投诉、改期这些异常分支。我第一版就漏了“服务中改期”这个场景导致后来补表结构找了不少麻烦。建议建表之前把所有可能的订单状态枚举和流转关系先画出来哪怕在纸上画都行。1.2 为什么选SpringBoot Vue3 MyBatis这套组合选这套组合主要从几个维度考虑SpringBoot的优势是生态成熟、起步快。SpringBoot 2.7.x 版本选得比较多3.x 虽然已经发布很久但如果你要用到一些老的第三方starter兼容性容易出问题。我这次用的是SpringBoot 2.7.18稳定资料多网上遇到问题基本都能搜到解决方案。Vue3比 Vue2 的响应式系统更高效Composition API 在写复杂业务组件的时候逻辑抽离特别舒服。我前端用了 Vite 作为构建工具启动速度快热更新也快比之前用 webpack 的时候舒服太多了。MyBatis半自动ORMSQL可控性强适合业务逻辑复杂、查询条件多变的系统。家政平台的订单列表筛选条件特别多按状态、时间、服务类型、城市、阿姨名字用MyBatis动态SQL处理起来非常顺手。MySQL 8.0是目前主流版本性能、窗口函数、JSON类型都比5.7强安装配置也不算复杂。数据量初期单表完全扛得住不用一上来就上分布式那套徒增复杂度。这四样组合在一起门槛不高资料丰富团队招人也容易。关键是完全开源免费项目部署在服务器上不需要额外掏授权费。1.3 功能模块划分我把系统拆成了三个端对应的目录结构也分开用户端小程序/H5/Web服务浏览、下单支付、订单管理、评价投诉、个人中心。阿姨端App/H5接单、服务状态更新、收入明细、服务评价、排班日历。管理后台Web服务类目管理、订单派单、阿姨审核与管理、用户管理、投诉处理、结算管理、数据概览。后端模块我按业务域划分包结构比如controller、service、mapper、entity、dto、vo下面是简化版的目录参考com.example.homecare ├── controller // 控制层 │ ├── admin // 后台管理接口 │ ├── user // 用户端接口 │ └── worker // 阿姨端接口 ├── service // 业务层 ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体 ├── dto // 入参对象 ├── vo // 出参对象 ├── config // 配置类拦截器、跨域、CORS等 └── common // 通用返回结果、异常处理、工具类前后端分离的核心是约定好接口文档。我用的是knife4j来生成在线接口文档前端直接看Swagger页面联调省了不少沟通成本。2. 数据库设计与核心表结构2.1 核心数据表规划数据库我建了大概20张表核心的几张开出来用户表userCREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, phone varchar(20) NOT NULL COMMENT 手机号, password varchar(100) DEFAULT NULL COMMENT 密码(BCrypt加密), nickname varchar(50) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint NOT NULL DEFAULT 1 COMMENT 1-用户 2-阿姨 3-管理员, status tinyint DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意一个用户多角色的问题。如果用户既是下单客户又注册成了阿姨role字段只能用user_role关联表来支持多角色。我第一版直接把role做成单值字段后来发现这种需求很常见补了一张user_role关系表才算彻底解决。服务类目表service_categoryCREATE TABLE service_category ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, parent_id bigint DEFAULT 0 COMMENT 父级分类id0表示顶级, icon varchar(255) DEFAULT NULL, sort_order int DEFAULT 0, status tinyint DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表orders是核心中的核心字段较多我列出关键的:CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 下单用户, worker_id bigint DEFAULT NULL COMMENT 接单阿姨, category_id bigint NOT NULL COMMENT 服务类目, service_time datetime NOT NULL COMMENT 预约服务时间, address varchar(255) NOT NULL COMMENT 服务地址, amount decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint NOT NULL COMMENT 状态1待支付 2待派单 3待服务 4服务中 5待评价 6已完成 7已取消, remark varchar(500) DEFAULT NULL, cancel_reason varchar(255) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_worker_id (worker_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单号我建议不要用数据库自增ID直接暴露给前端容易被人遍历抓数据。用时间戳随机数的方案生成即可例如yyyyMMddHHmmss 6位随机数或者直接用雪花算法但内网单机部署雪花算法意义不大简单方案就够用。2.2 状态字段的设计取舍订单状态字段我用的tinyint注释里写明每个数字代表什么。有人喜欢用varchar存状态字符串比如WAIT_PAY、FINISHED可读性是好了但存储和索引效率都不如tinyint。我的妥协方案是数据存tinyint在Java枚举类里把数字和描述映射好代码里可读性完全不差数据库还清爽。枚举类大致长这样public enum OrderStatus { WAIT_PAY(1, 待支付), WAIT_ASSIGN(2, 待派单), WAIT_SERVICE(3, 待服务), SERVING(4, 服务中), WAIT_COMMENT(5, 待评价), FINISHED(6, 已完成), CANCELED(7, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }每次状态变更必须做状态机校验不能允许从“待支付”直接跳到“已完成”。这个校验逻辑写在Service层确保并发情况下状态流转也是安全的。2.3 MySQL 8.0 的安装与初始化注意点如果你还没装MySQL我用Windows环境简单说一下坑点。MySQL 8.0安装包在官网下载选Server only就行开发机没必要装全家桶。装完以后需要注意两个地方第一默认字符集。8.0默认是utf8mb4比5.7的默认配置好了很多但我还是建议在my.ini里显式配上[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci [client] default-character-setutf8mb4不然Windows控制台连上去之后中文可能出现乱码。第二用Navicat或命令行工具连库的时候密码加密规则可能会报错如果客户端太老需要执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;开发阶段用root问题不大上了生产环境建议单独建个业务账号只授权项目数据库的权限别给全库权限。3. 后端SpringBoot搭建细节3.1 项目初始化与依赖选型SpringBoot项目我习惯直接用 https://start.spring.io 生成基础骨架选好Java版本我用JDK 8稳定至上和SpringBoot版本然后手工往pom.xml里加依赖。核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis Spring Boot Starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok 简化实体代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency再提一个很多人忽略的问题SpringBoot版本和JDK版本的兼容性。SpringBoot 2.7.x 可以用JDK 8但SpringBoot 3.x 最低要求JDK 17。如果服务器上只装了JDK 8你硬用3.x就起不来报错信息里会有类似java.lang.UnsupportedClassVersionError这就是版本不匹配的典型症状。开发前先确认环境别浪费一晚上排查一个版本兼容问题。application.yml里的配置也很关键我把MyBatis的配置单独列一下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/homecare?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.homecare.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置强烈建议打开数据库字段create_time就能自动映射到Java属性createTime不然每个字段都得在XML里手动写resultMap麻烦得很。log-impl设置成StdOutImpl开发阶段控制台会打印完整SQL和参数排查问题非常有用。生产环境记得关掉。3.2 MyBatis集成与XML映射MyBatis支持注解和XML两种方式写SQL我的原则是简单的CRUD用注解复杂的动态查询用XML。比如根据订单状态列表查询动态条件多用XML就方便很多select idqueryOrderList resultTypecom.example.homecare.vo.OrderVO SELECT o.*, u.nickname as userNickName, w.real_name as workerName FROM orders o LEFT JOIN user u ON o.user_id u.id LEFT JOIN worker_profile w ON o.worker_id w.user_id where if teststatus ! null AND o.status #{status} /if if testuserId ! null AND o.user_id #{userId} /if if testworkerId ! null AND o.worker_id #{workerId} /if if teststartTime ! null AND o.create_time gt; #{startTime} /if if testendTime ! null AND o.create_time lt; #{endTime} /if /where ORDER BY o.create_time DESC /selectwhere标签会自动处理掉第一个AND这是我用得最多的MyBatis特性。再加一个if判断基本上订单列表那种多条件组合查询就稳了。如果表不存在需要自动建表MyBatis本身不干这个活儿这是数据库迁移工具做的事。简单方案是SpringBoot整合Flyway把建表SQL放进src/main/resources/db/migration目录项目启动时自动执行。这种表结构版本管理方式比手动在Navicat里点执行靠谱太多了多环境部署的时候能少很多事。3.3 订单状态流转与并发处理家政平台的订单在“用户支付完成”这个节点最容易出现并发问题。用户连续点击两次“支付”如果没有幂等处理很可能会生成两条相同内容的订单或者重复扣款。我的处理方案是支付回调接口里先根据订单号加行锁或使用乐观锁版本号机制再判断当前状态是否允许变更。核心代码简化一下Transactional(rollbackFor Exception.class) public boolean payCallback(String orderNo) { // 使用SELECT FOR UPDATE锁住订单行 Orders order orderMapper.selectByOrderNoForUpdate(orderNo); if (order null) { throw new RuntimeException(订单不存在); } // 当前状态必须为待支付 if (order.getStatus() ! OrderStatus.WAIT_PAY.getCode()) { // 已处理过直接返回成功实现幂等 return true; } // 更新状态为待派单 int rows orderMapper.updateStatus(orderNo, OrderStatus.WAIT_PAY.getCode(), OrderStatus.WAIT_ASSIGN.getCode()); if (rows 0) { throw new RuntimeException(订单状态更新失败); } return true; }selectByOrderNoForUpdate对应的SQL是select idselectByOrderNoForUpdate resultTypeOrders SELECT * FROM orders WHERE order_no #{orderNo} FOR UPDATE /select加了FOR UPDATE之后这条订单记录在被提交前其他事务都无法修改天然避免并发下的订单状态错乱。updateStatus对应的SQL是关键update idupdateStatus UPDATE orders SET status #{newStatus} WHERE order_no #{orderNo} AND status #{oldStatus} /update这种带旧状态的更新条件即便两个线程同时进来也只有一个能成功另一个更新行数为0程序就知道不该再处理了。这个方法我没在教科书写过但实际并发测试过靠这个方案兜底就稳了。3.4 用户登录与权限控制登录认证我用的是JWT 拦截器的方案没有引入Spring Security那套重武器。JWT工具类负责生成和解析token登录成功之后把token返回给前端前端存在localStorage里每次请求在请求头里带上Authorization: Bearer token。拦截器里校验token有效性同时把当前用户信息放入ThreadLocal方便业务代码直接取public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); // 解析token失败则返回401 if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } try { Long userId JwtUtil.parseToken(token); UserContext.setCurrentUserId(userId); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }管理员接口和普通用户接口用RequireRole自定义注解加拦截器校验比Spring Security配置简单得多够用就行。密码加密一定要用BCrypt别用MD5。MD5撞库容易得很BCrypt每次生成的哈希值都不同安全性高很多。Spring Security单独引包有点重可以用spring-security-crypto这个轻量模块。4. 前端Vue3实现与前后端联调4.1 Vue3项目创建与路由设计前端我用的Vite创建Vue3项目npm create vitelatest homecare-admin -- --template vue cd homecare-admin npm install npm install vue-router4 pinia axios element-plus管理后台UI用的Element Plus用户端和阿姨端条件允许的话可以单独做移动端适配我用的是Vant。路由设计的核心是权限控制。我前端根据用户角色动态生成路由表管理员登录后能看到用户管理、阿姨管理、订单派单等菜单普通用户登录后看不到这些。这部分用Pinia存储用户角色和可访问路由配合路由守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } // 动态路由已加载直接放行 if (router.hasRoute(to.name)) { next() return } // 尝试根据用户角色动态注册路由 const userStore useUserStore() if (userStore.routes.length 0) { userStore.generateRoutes().then(() { next({ ...to, replace: true }) }) return } next() })Vite配置开发环境代理也很关键解决前端请求后端的跨域问题不用后端额外配置CORS// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/user/login实际转发到后端的http://localhost:8080/api/user/login前端代码里就不用写完整域名了。4.2 Axios封装与跨域问题Axios封装是前端项目的最基础工程。我做了一个封装统一处理token附加、错误提示、401跳转等逻辑import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器附加token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request封装的思路是业务代码里不需要关心token怎么带、错误弹窗怎么处理统一收口在这两个拦截器里改起来也只需要改一处。4.3 家政平台关键页面实现思路管理后台最核心的页面是订单管理和派单操作。订单管理页面就是表格加筛选条件筛选栏用Element Plus的下拉选择、日期选择、输入框组合。表格里显示订单号、用户、服务类目、金额、状态、操作按钮。操作按钮根据状态动态显示比如“待派单”状态显示“派单”按钮“待评价”状态显示“查看评价”按钮。派单操作弹出一个对话框里面是一个阿姨列表显示阿姨的评分、接单数、距离如果接入了LBS管理员选中后点击确认调用后端派单接口。实现这个页面要注意的是表格数据的刷新时机。我习惯分页组件切换、筛选条件变化、操作完成之后都重新拉取数据保证页面永远显示最新数据。用户端的服务列表和下单流程相对简单但要注意微信H5的兼容性调试Vue3项目在低版本安卓WebView上偶尔会出现白屏需要在入口文件里做Polyfill兼容这个等真遇到再处理也来得及。5. 常见问题排查与实战避坑5.1 经典报错与解决方案速查表报错信息原因解决方案java.lang.ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动版本不对或没引入引入mysql-connector-j确认SpringBoot版本对应驱动坐标Access denied for user rootlocalhost账号或密码错误、权限问题检查密码确认root账号的host允许本地登录Table xxx doesnt exist表没建成功或库连错检查数据库名执行建表脚本Invalid bound statement (not found)Mapper接口和XML映射没对应上检查XML的namespace、id是否与接口方法匹配确认mapper-locations配置正确Error attempting to get column xxx from result set查询结果有字段无法映射到实体类检查是否开了map-underscore-to-camel-case字段类型和Java类型是否匹配前端请求跨域Network Error代理或CORS配置不对开发环境用Vite的proxy生产环境用Nginx代理二者选一SpringBoot启动失败端口被占用本地8080端口被占用netstat -anojava.lang.UnsupportedClassVersionErrorJDK版本和编译目标不匹配统一JDK版本SpringBoot 3.x必须JDK 17我当时最头疼的一个问题是“invalid bound statement”折腾了俩小时才发现mapper-locations配的路径不对XML文件没被扫描到。如果你也遇到这个问题优先检查target目录里有没有生成对应的XML文件没生成就说明路径配错了或者文件放错位置了。5.2 关于MyBatis缓存和打印SQL的那些事这里说两点容易被骂的细节。第一MyBatis一级缓存默认是开启的。一级缓存在SqlSession生命周期内有效但在项目里每次数据库操作基本都是重新获取SqlSession所以一级缓存实际上没用上。二级缓存默认是关闭的我建议超过一个表以上的查询都别开二级缓存因为关联表更新时缓存刷新容易出脏数据。线上真需要缓存直接用Redis自己控制别依赖MyBatis内置缓存。第二SQL打印配置别在生产环境一直开着。StdOutImpl会把所有SQL日志打到控制台性能损耗虽然没有特别大但日志文件会迅速膨胀而且如果SQL里带用户手机号、地址这些敏感信息日志文件的管理也是个风险点。开发和测试环境开着没问题生产环境用Logback配置把MyBatis日志级别调到WARN。5.3 一些提升效率的开发小技巧技巧一热部署。后端加一个spring-boot-devtools依赖改完代码自动重启开发体验提升一个档次。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId optionaltrue/optional /dependency技巧二用MyBatis Code Helper这类IDEA插件。从数据库表直接生成实体类、Mapper接口和XML文件省去手写一堆模板代码的时间。技巧三Postman或Apifox管理接口。我自己习惯用Apifox因为可以把线上文档和调试工具结合接口变动时文档同步更新。开发阶段写接口文档这件事能自动化就自动化手动写文档99%会过期。技巧四mock数据趁早做。家政平台的测试数据填充挺费劲的我后来写了个CommandLineRunner项目启动时检测数据库为空就自动插入测试阿姨、服务类目、用户账号。每次开发联调都有现成数据可以玩省了很多时间。6. 一些更深入的拓展方向这个平台当前版本的核心链路已经跑通了但距离真正上线商用还有几块可以继续深挖。支付接入。现在的支付回调是模拟的真实上线需要对接微信支付、支付宝的Native支付或H5支付。核心逻辑是创建订单时同步生成支付二维码用户支付后微信/支付宝服务器异步回调后端接口这个回调接口必须做签名验证和幂等处理。消息通知。订单状态变化的时候用户需要收到通知。小程序用订阅消息短信用来做验证码和催单提醒。如果要上这个可以提前在订单表加一个notify_status字段或者单独建一张message_record表避免后面补数据。地图定位和距离计算。家政服务很多场景要看距离比如按距离排序推荐最近的阿姨。实现不复杂MySQL 8.0内置了空间计算函数ST_Distance_Sphere可以直接根据经纬度算出两个位置之间的球面距离单位米。这块等真有了LBS需求再引入初期把地址存成文本就够了。数据统计看板。管理员首页的大屏数据今日订单数、营收、完成率、好评率、阿姨分布等。技术实现就是在order表上跑聚合SQL或者定时任务把统计数据刷进一张汇总表。要注意的是统计维度多的话聚合查询会越来越慢单独建一张stat_daily表每天凌晨跑定时任务汇总昨天数据查询的时候毫秒级返回体验比实时聚合好很多。我个人在实际项目里的体会是家政服务平台这种系统技术栈不是瓶颈真正考验的是业务逻辑的完整度。状态流转是否严谨、异常分支是否覆盖、权限粒度是否合理这些才是决定项目能不能真正用起来的核心。先把这些基础打牢后面的功能扩展就是往上叠积木的事儿。最后分享一个我踩过的坑吧联调阶段一定要让前端用真实接口文档不要“我在本地测过了给你几个curl命令”。前端和后端各自在本地测过没问题一旦真正联调字段名对不上参数类型不对的问题一大把。一开始就把apifox或者swagger之类的接口文档定好联调阶段的问题能在一天内全清干净。