ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

家政保洁预约系统实战:从订单状态机到派单部署全解析

家政保洁预约系统实战:从订单状态机到派单部署全解析 先说个真实场景很多家政公司现在的日常是微信群派单、Excel排班、阿姨上户靠电话确认用户不知道保洁员几点到、服务到哪一步出了问题只能在群里客服。这不是管理问题是流程数字化的问题。家政保洁预约系统要做的就是把预约-派单-服务-结算-评价整条链路搬到线上让用户、保洁员、管理员三方都在同一个信息平面上协作。这篇文章基于我自己从零到一实现家政保洁预约系统的完整过程覆盖业务梳理、技术选型、数据库设计、订单状态机、派单逻辑、小程序前端交互以及真实部署中踩过的坑。不堆理论全部是可落地的设计决策和代码思路。适合正在做类似系统设计的开发者、准备做毕设或课程设计的同学也想给家政公司做内部系统的朋友参考。1. 家政保洁预约系统到底在做什么先捋清业务模型很多开发者拿到这类系统需求第一反应就是建表、写接口。这是典型的顺序错误。家政保洁系统最核心的复杂度不在增删改查而在业务规则——谁在什么条件下能做什么事订单在什么状态下能流转到哪一步。先把这些规则想清楚后面的开发就是体力活。1.1 三个角色、四条主流程这就是系统的全部骨架这个系统里有三类使用者用户、保洁员、管理员。用户在小程序/App端选服务、下单、支付、查进度、评价保洁员在接单端抢单或接收派单、开始服务、结束服务、查看收入管理员在后台维护服务项目管理、保洁员资质审核、订单调度、退款处理以及数据统计。四条主流程分别是预约下单流程用户选择服务项目选择期望上门时间填写地址生成订单并支付或先下单后支付。派单与接单流程新订单进入派单池管理员手动派单或系统按距离、评分、接单量自动派给合适保洁员保洁员确认接单。服务执行流程保洁员到户后点击开始服务服务完成后点击结束服务用户确认后订单完成。结算与评价流程订单完成后触发结算平台抽成后生成保洁员收入记录用户可对服务进行评价和打赏。为什么说是四条主流程而不是六条因为支付退款、取消订单、投诉售后这些虽然重要但不是日常高频操作它们挂在主流程上做状态分支即可不需要单独设计一套业务线。1.2 从业务规则到功能清单哪些必须有哪些可以砍我在第一版开发时列过一个功能清单后来发现有些功能纯属给自己加戏比如保洁员动态定位追踪。这个功能听起来很酷但实现要接地图SDK、要保洁员端持续上报位置、要用户端画轨迹开发量大且实际使用率很低。用户真正关心的是保洁员还有多久到一个到店倒计时就够用了。真正上线就必须要有的功能模块必备功能备注用户端服务项目浏览、选日期时间、地址管理、下单支付、订单进度查看、评价支付用微信支付预下单到回调全链路必须稳定保洁员端待接单列表、接单/拒绝、开始服务/结束服务、收入明细接单逻辑要考虑并发同一订单不能被两人接到管理后台订单管理、服务项目管理、保洁员审核与管理、派单操作、退款审核、月度营收统计派单操作要支持改派这是家政业务特别常见的场景可以砍掉或者放到二期做的功能用户积分体系、保洁员在线培训、优惠券裂变、GPS轨迹回放。这些功能没有不影响系统流转加了反而牵扯大量页面和规则设计第一版能跑通闭环比什么都重要。2. 技术选型的思考过程为什么是这套组合而不是别的家政预约系统不是高并发互联网应用它是个典型的业务管理系统。技术选型的原则就一条用熟悉且稳定的技术把业务逻辑表达清楚不为技术而技术。2.1 后端Spring Boot 3 MyBatis-Plus不用微服务的理由后端我选的是 Spring Boot 3.2 JDK 17 MyBatis-Plus 3.5.5。这个组合是目前中小型管理系统最稳妥的方案Spring Boot 负责整体框架和自动化配置MyBatis-Plus 负责数据访问单表CRUD基本不用写SQL。为什么不上微服务家政平台起步阶段的用户量就是几千到几万单机部署一台2核4G的云服务器完全扛得住。微服务带来的注册中心、配置中心、网关、链路追踪这些组件每个都要运维成本对一个小团队来说是纯负担。项目结构上我是这样分包的简单直接com.homeclean ├── controller // 接口层只做参数校验和结果封装 ├── service // 业务层订单状态流转、派单逻辑全在这 ├── mapper // 数据访问层MyBatis-Plus的BaseMapper ├── entity // 数据库实体 ├── dto // 接收前端参数的对象 ├── vo // 返回给前端的对象 ├── config // 配置类如微信支付、Redis、定时任务 ├── common // 统一返回结果、异常处理、常量 └── task // 定时任务超时未支付关单、服务完成提醒等2.2 Redis 在系统里的三个具体用途第一版我差点没上 Redis觉得业务量小不需要。结果联调的时候发现两个 mvcc 都没法解决的问题一是用户重复点击下单按钮会创建多条订单二是多个保洁员同时抢同一订单时会出现超接。这些问题用数据库锁也能处理但会拖慢性能并且代码写起来非常别扭。Redis 在这里做了三件事防重复提交用户点击下单时用userId serviceItemId scheduledTime生成唯一keySETNX成功才继续失败直接提示请勿重复下单。分布式锁保洁员抢单时以orderId为锁key抢到锁的保洁员才能更新订单防止同一订单被两名保洁员同时接走。首页服务项目缓存服务项目列表、热门项目这些不常变化的接口缓存到 Redis缓存时间设 30 分钟减少数据库压力。2.3 定时任务两个容易被忽略的场景家政预约系统里有两个场景必须靠定时任务不能靠用户操作触发超时未支付订单关闭用户下单后 30 分钟未支付订单自动取消。这个我用 Spring 自带的Scheduled每 2 分钟扫描一次把待支付超时的订单批量关单。服务完成后的待评价提醒订单完成后 24 小时内用户可以评价超时未评价则自动默认好评。同样用定时任务处理。有两点经验值得说一是定时任务要加分布式锁防止多实例部署时重复执行我用 Redis 的SETNX加锁二是定时任务只处理状态字段具体的业务操作如取消订单、关闭支付要复用和接口相同的 service 方法避免逻辑分叉。3. 数据库设计核心思路订单表不是简简单单的订单表数据库是家政系统的地基。表结构设计有问题后面写代码全是别扭。我在设计时花的时间最长的是订单表和各种快照字段这里有挺多会踩的坑。3.1 核心表结构与关系系统的核心表一共十张用户表、保洁员表、管理员表、服务项目表、用户地址表、订单表、订单服务明细表、评价表、保洁员收入流水表、退款表。其中最核心的是三张订单主表order一条订单记录包含订单号、用户id、保洁员id可空派单前为空、服务项目id冗余服务名称快照、预约时间、实际开始时间、实际结束时间、订单金额、优惠金额、实付金额、支付状态、订单状态、用户地址快照、联系人、联系电话、备注、创建时间、完成时间。保洁员收入流水表cleaner_income订单完成时生成包含订单号、保洁员id、订单金额、平台抽佣比例、抽佣金额、结算金额、状态待结算/已结算。服务项目表service_item服务名称、服务时长、标准价格、单位按次/按时、描述、状态。3.2 订单状态为什么推荐用字符串而不是数字下面的表和字段是很多教程不会写到的我在这里栽过跟头写出来给各位避坑。订单状态我用字符串枚举而不是数字因为字符串在日志里一眼能看懂排查问题不用对照表。线上跑起来之后翻日志看到status FINISHED就知道订单完成了如果是3还得去代码里查 3 代表什么。订单状态流转PENDING_PAYMENT待支付 → 用户支付 → PAID已支付/待派单 → 超时未支付 → CANCELED已取消 PAID已支付/待派单 → 管理员派单 / 保洁员接单 → DISPATCHED已派单 DISPATCHED已派单 → 保洁员开始服务 → IN_SERVICE服务中 IN_SERVICE服务中 → 保洁员结束服务 → FINISHED已完成 FINISHED已完成 → 用户评价 → EVALUATED已评价 → 24小时未评价自动完成 → EVALUATED已评价所以我在订单表里是用一个order_status字段维护状态pay_status单独拆出来。这两个状态很容易混在一起。订单状态是业务的进度支付状态是资金的状态——用户可能先支付了服务还没开始也可能服务完成了但还在退款流程里它们本来就不是一一对应关系。3.3 两个订单表字段设计的取舍地址快照字段订单表里冗余存了address_detail和contact_name、contact_phone。为什么不直接关联用户地址表因为用户可能改了默认地址但历史订单应该保留下单时的地址。快照的目的就是让历史订单不被后续操作影响。价格快照字段服务项目表的价格可能会调整但已生成的订单金额不能跟着变。所以订单表里要冗余存service_name、unit_price、total_amount这些字段。这两个设计思路可以概括为一句话订单表里凡是用户下单时确定的业务数据都要做快照不做快照的后面调价改名时就会出事故。4. 订单状态机与派单整个系统最容易写崩的两块前面把表和字段定好了接下来进入最核心的代码逻辑部分。先说状态机再讲派单这两块是家政预约系统区别于普通 CRUD 系统的分水岭也是我在开发中 Debug 到头疼的地方。4.1 状态机设计把所有状态迁移写清楚状态机不是玄学它解决的是当前状态下这个操作合不合法的问题。保洁员在PENDING_PAYMENT状态下点开始服务应不应该成功不应该因为用户还没付钱。游客能对FINISHED的订单申请退款吗不应该因为已经超过售后期了。我的做法是写一个枚举类OrderStatusEnum把每个状态的合法流转记录下来public enum OrderStatusEnum { PENDING_PAYMENT(0, 待支付), PAID(1, 已支付/待派单), DISPATCHED(2, 已派单), IN_SERVICE(3, 服务中), FINISHED(4, 已完成), EVALUATED(5, 已评价), CANCELED(6, 已取消); // 检查当前状态能否流转到目标状态 public boolean canTransitTo(OrderStatusEnum target) { switch (this) { case PENDING_PAYMENT: return target PAID || target CANCELED; case PAID: return target DISPATCHED || target CANCELED; case DISPATCHED: return target IN_SERVICE; case IN_SERVICE: return target FINISHED; case FINISHED: return target EVALUATED; default: return false; } } }这个设计很老土但极其实用。每次服务调用状态流转变更前先执行合法性校验不合法直接抛异常从代码层面杜绝了非法操作。后面写再多的业务逻辑状态机是唯一的出入口不易出错。4.2 派单策略手动派单为主自动派单为辅改派必须存在家政行业的派单逻辑比外卖复杂。外卖骑手是系统自动匹配不用用户确认家政保洁员服务的是陌生人用户对保洁员有信任门槛——很多老客户会指定熟悉的保洁员不接受随机分配。所以家政预约系统的派单策略是用户指定用户下单时可以选择指定保洁员如果该保洁员有档期或平台安排。管理员手动派单平台安排的单进入派单池由管理员在后台列表操作选人、改派都很方便。自动派单可配置开关系统按距离最近 评分最高 当日接单量最少三项指标加权排序自动分配给第一位保洁员保洁员端收到推送确认接单。改派是家政管理后台里非常容易被忽略但实际天天用到的功能。保洁员接单后用户取消、保洁员临时家里有事、服务过程中出现纠纷都需要管理员把订单改派给其他人。改派的时候要同时处理原保洁员的取消通知和新保洁员的接受逻辑这一块代码量不大但流程细节多。4.3 抢单并发问题的完整解决方案派单池模式有一个典型的并发场景自动派单把订单推给了评分最高但当天已经接了4个单的保洁员A保洁员A 10秒内没点接单系统超时回收重新派给了保洁员B。这时候A刚好点了接单会怎样如果代码不做并发控制后面成功的更新会覆盖前面的订单被两个人同时接走或者一个接单请求覆盖掉另一个的派单记录状态就乱了。我在代码里用 Redis 分布式锁解决public boolean grabOrder(Long orderId, Long cleanerId) { String lockKey ORDER_LOCK: orderId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, cleanerId.toString(), 10, TimeUnit.SECONDS); if (!locked) { // 没拿到锁说明别人正在处理这个单 return false; } try { // 二次校验订单状态必须还是“已派单且无人接单” Order order orderMapper.selectById(orderId); if (!OrderStatusEnum.PAID.getCode().equals(order.getOrderStatus())) { return false; } // 执行接单更新同时带上 status PAID 的条件做数据库层兜底 int rows orderMapper.grabOrder(orderId, cleanerId); return rows 0; } finally { redisTemplate.delete(lockKey); } }这样的设计用了三层防护Redis 锁挡住并发请求、状态二次校验挡住非法操作、SQL 条件更新作为最后的原子性保证。实际使用下来即使多端同时操作也没有再出现过重复接单的情况。5. 前端交互设计用户端和保洁员端完全两种思路家政预约系统的前端我分成了三个小程序端用户端小程序、保洁员端小程序、管理后台H5架构上复用同一套后端 API用不同的角色权限控制访问范围。5.1 用户端核心页面预约下单流程的体验设计用户端的核心链路是浏览→选服务→选时间→填地址→支付→跟踪进度。这里最需要打磨的是选择时间这个环节。家政服务的预约时间不能只给一个简单的日期选择器一定要做日期时间段两级选择。实际业务里用户选周六上午 9 点到 11 点系统要判断周六上午 9 点有没有保洁员可派所以前端需要把可选时间段查出来返回。我实现的接口是前端传入serviceItemId和要查询的日期后端根据这个日期当天已有订单数、在班保洁员数量、服务时长如 3 小时算出可预约的时间段。已排满的时间段返回disabled: true前端渲染成灰色不可点。另外评价功能要放在订单结束后 24 小时内可用过了窗口自动默认好评所以我加了 24 小时倒计时。前端从订单详情拿evaluateDeadline字段做倒计时渲染而不是自己算。5.2 保洁员端轮询还是 WebSocket保洁员端有一个待接单列表。最开始我用的是 WebSocket 长连接实时推送后来发现一个问题保洁员在外面干活时经常断网、切后台 、小程序被回收长连接可靠性反而不如简单轮询。最终方案是** WebSocket HTTP 轮询双通道**小程序切前台时主动调接口刷新订单列表后台驻留时靠 WebSocket 推送通知并更新角标。WebSocket 连不上就靠轮询兜底反正家政平台的单量远远到不了轮询有压力的程度。5.3 管理后台的核心操作效率管理后台有个很容易被忽略的需求管理员每天要处理大量派单和改派操作列表页必须支持快捷操作不能每次操作都要点进详情页。我做的优化是列表直接提供派单按钮点开后是一个抽屉里面有可派保洁员列表按距离排序、标注今日接单数管理员直接选择确认派单。订单列表默认只展示待派单状态的单减少管理员筛选成本。6. 真实开发中遇到的坑和排查思路记录到哪一步代码就崩在哪一步这一部分分享几个很有代表性的踩坑和排查思路。开发之前看这部分会觉得这不都挺简单的吗上线后遇到才会明白为什么要这么处理。6.1 支付回调幂等同一个通知发了三四遍微信支付的回调通知官方说明是会多次推送商户系统必须能够正确处理重复通知。我第一次实现时只校验了订单金额结果测试中发现重复回调导致订单状态被覆盖、收入流水多生成了一条。整个排查链路是这样的测试时用 0.01 元小额支付支付成功后连续发起多次回调发现订单状态从PAID被改成了PAID还无所谓但收入流水表里多了好几条记录金额也重复累加了。看代码才意识到我的收入流水的创建直接在支付回调里没有做幂等校验。修复的思路其实很简单支付回调里对订单号做唯一约束收入流水表的 order_id 字段建唯一索引插入时捕获重复异常直接视为成功不再重复创建。同时状态更新里加一个条件判断只有当前状态是PENDING_PAYMENT才能改成PAID重复回调根本进不来。6.2 定时任务和支付回调并发状态被覆盖了再分享一个更隐蔽的坑。我原本以为超时未支付关单的任务会和支付回调同时执行——比如用户刚好在超时前 1 秒支付定时任务已经开始扫描该订单并执行了取消订单操作这时候支付回调进来看看状态不对忽略掉。结果线上真出现了一单用户付了钱订单却显示已取消。排查链路的根因在于定时任务批量关单的 SQL 是直接UPDATE order SET status CANCELED WHERE id ? AND status PENDING_PAYMENT理论上带上了状态条件不会出错。但问题出在支付回调的判断条件上我当时的写法是if (!OrderStatusEnum.PENDING_PAYMENT.getCode().equals(order.getOrderStatus())) { return; // 忽略非待支付订单 }这段代码看着没毛病但它查出来的order对象是支付回调入口时查的查出来时还是PENDING_PAYMENT接着定时任务把状态改成了CANCELED回调这边拿着旧对象继续往下执行更新的时候又没有带状态条件直接就把取消的订单改回已支付了。修复方式还是那句话一切状态更新都必须在 SQL 里带条件。支付成功的更新 SQL 必须是UPDATE order SET status PAID WHERE id ? AND status PENDING_PAYMENT如果更新影响行数为 0 说明状态已经变了不再执行后续的流水创建等操作。加上这一层后定时任务和支付回调并发的问题再没出现过。6.3 保洁员结算金额的精度问题钱的东西千万不能掉以轻心。订单表金额字段一律用DECIMAL(10, 2)Java 实体用BigDecimal不允许用double或float。抽佣比例用 0.1 这种小数计算时用BigDecimal.multiply并指定RoundingMode.HALF_UP。我还遇到过一个看起来很小但影响面很广的问题订单实际结束时间和预约时间不一致。比如用户约了 2 小时保洁员干 3 小时这时候是否要加收超时费业务上目前没做超时自动加价但结算逻辑必须考虑服务时长是从开始到结束的真实时长而不是预约时长。所以收入流水的结算金额要用订单实际金额来算我这张流水表里专门存了actual_amount和settle_amount两个字段就是为了避免后续争议时财务和后台两边对不上账。7. 部署上线与运营后续的补充一台服务器怎么跑起来测试没问题不代表上线没问题。家政预约系统上线要过的关口还挺多的从服务器配置到域名备案、小程序审核、支付商户号申请一环扣一环。这里讲一下最实用的部署方案和几个上线后必须盯的指标。7.1 部署方案一台2核4G的云服务器能扛住什么量级根据实际压测经验一台2核4G的云服务器部署以下服务完全没有压力Nginx 反向代理 HTTPS 证书Spring Boot 后端 jar 包配置 JVM 堆内存 -Xmx2gMySQL 8.0数据量在十万级订单以内无压力Redis 7.0缓存 分布式锁我的上线部署流程是用 docker-compose 编排 MySQL 和 Redis后端直接用nohup java -jar跑系统进程Nginx 挂在外面做反向代理。用 docker 跑 MySQL 和 Redis 的意义在于方便数据卷备份和快速重建后端不打 docker 是因为小团队改代码后直接替换 jar 重启最快没必要每次构建镜像。7.2 上线后必盯的指标先看这五个系统跑起来之后每天要看的不只是营收。真正能反映系统健康度的指标是哪些我自己的运维看板里固定看这五个待派单订单量超过 30 分钟还没派出去的单说明运力不足或派单策略有问题需要人工介入。超时未支付关单率这个比例高说明用户下单流程有阻力可能价格不合适或支付体验有问题。支付回调失败次数正常应为 0一旦出现要立刻去看日志否则用户付了钱订单没变化客诉直接爆炸。保洁员接单响应时长单均派单到接单的时间超过 5 分钟要检查短信/小程序订阅消息有没有问题。用户取消率用户主动取消订单的比例超过 20% 就要考虑是不是预约时间匹配度做得不好。7.3 用户通知链路订阅消息必须要做兜底最后说一个家政系统特别典型的运营细节订单流转的各种通知——用户下单成功通知、保洁员接单通知、服务开始提醒、订单完成评价提醒。微信小程序用订阅消息但订阅消息有个限制用户一次授权只能发一次比如用户下单时授权了接单通知派单成功就能推一条但后面服务开始提醒还需要用户另外授权。实际运营中根本做不到让用户每次都点授权。所以我的兜底方案是短信通知尤其是保洁员接单和用户订单被接受这两个关键节点。短信虽然要花钱但一条只有几分钱相对于用户投诉造成的损失完全可以接受。还有一个操作是最开始没想到的——很多用户不看微信消息但在服务开始前半小时收到一条保洁员预计 X 分钟后到达的短信体验感知会强很多。设计实现一套家政保洁预约系统说到底是把线下家政的人情生意变成线上可追踪的标准化流程。整个过程最花时间的不是写代码而是把业务规则梳理清楚。我在第一版时也犯过无脑加功能、过度设计的毛病后来根据实际运营反馈砍了不少。如果让我给正在做同类系统的人一个建议那就是先把订单状态机、派单并发、支付回调这三块打磨稳剩下的功能再多再炫这三块是系统的命门。地域性的家政公司从预约到结算全流程跑通之后人力调度效率能提升一倍以上这个提升在运营数据里不仅仅反映为订单量还反映为用户主动复购和好评率。
返回列表