家政服务平台,前后端分离实战复盘:SpringBoot+Vue3+MyBatis这套组合怎么落地最省心
之前在做一个家政服务类的接单平台,需求倒不复杂,但涉及的角色、状态流转、订单拆分、结算逻辑这些叠在一起,如果一上来就乱写,后期维护绝对想骂人。我选型定的是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半自动ORM,SQL可控性强,适合业务逻辑复杂、查询条件多变的系统。家政平台的订单列表筛选条件特别多(按状态、时间、服务类型、城市、阿姨名字),用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张表,核心的几张开出来:
用户表(user)
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';注意一个用户多角色的问题。如果用户既是下单客户又注册成了阿姨,role字段只能用user_role关联表来支持多角色。我第一版直接把role做成单值字段,后来发现这种需求很常见,补了一张user_role关系表才算彻底解决。
服务类目表(service_category)
CREATE TABLE `service_category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `parent_id` bigint DEFAULT '0' COMMENT '父级分类id,0表示顶级', `icon` varchar(255) DEFAULT NULL, `sort_order` int DEFAULT '0', `status` tinyint DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表(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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单号我建议不要用数据库自增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-server=utf8mb4 collation-server=utf8mb4_unicode_ci [client] default-character-set=utf8mb4不然Windows控制台连上去之后,中文可能出现乱码。第二,用Navicat或命令行工具连库的时候,密码加密规则可能会报错,如果客户端太老,需要执行:
ALTER USER 'root'@'localhost' 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis Spring Boot Starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok 简化实体代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true 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 id="queryOrderList" resultType="com.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 test="status != null"> AND o.status = #{status} </if> <if test="userId != null"> AND o.user_id = #{userId} </if> <if test="workerId != null"> AND o.worker_id = #{workerId} </if> <if test="startTime != null"> AND o.create_time >= #{startTime} </if> <if test="endTime != null"> AND o.create_time <= #{endTime} </if> </where> ORDER BY o.create_time DESC </select><where>标签会自动处理掉第一个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 id="selectByOrderNoForUpdate" resultType="Orders"> SELECT * FROM orders WHERE order_no = #{orderNo} FOR UPDATE </select>加了FOR UPDATE之后,这条订单记录在被提交前,其他事务都无法修改,天然避免并发下的订单状态错乱。
updateStatus对应的SQL是关键:
<update id="updateStatus"> 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;charset=UTF-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 vite@latest homecare-admin -- --template vue cd homecare-admin npm install npm install vue-router@4 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.Driver | MySQL驱动版本不对或没引入 | 引入mysql-connector-j,确认SpringBoot版本对应驱动坐标 |
Access denied for user 'root'@'localhost' | 账号或密码错误、权限问题 | 检查密码,确认root账号的host允许本地登录 |
Table 'xxx' doesn't 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 -ano |
java.lang.UnsupportedClassVersionError | JDK版本和编译目标不匹配 | 统一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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</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之类的接口文档定好,联调阶段的问题能在一天内全清干净。