
每年到这个时间点都会有人来问 Spring Boot 家政服务平台这个毕设题怎么下手。说实话这个题目每年一批人做但大多数交上去的东西最后都长成了一个管理员可以增删服务分类的后台管理系统——答辩时一问到核心业务逻辑就卡壳。家政平台真正有含金量的地方从来不是 CRUD而是标题里那极速达三个字用户下单之后系统怎么快速匹配到合适的服务人员订单从发起到完结状态机怎么设计才不乱支付、退款、结算这条资金流又该怎么闭环。这篇文章我就把这整条链路拆开讲从数据库表设计到派单调度再到资金结算适合正在准备 springboot 毕设、尤其是想把这个题目真正做出亮点的同学参考。我尽量说实操不给空话。1. 极速达家政的快字诀先想清楚业务边界再动键盘1.1 三种角色各自要的是什么家政服务平台不是简单的用户和服务人员一对一关系至少要有三类角色同时在线运转普通用户浏览服务、下单预约、支付、查看服务进度、评价。服务人员接单、开始服务、完成服务、查看自己的结算流水。平台管理员审核服务人员入驻、管理服务类目、处理异常订单、查看平台统计数据。很多同学一上来就照着别人开源项目的表结构抄抄完发现用户表和角色表对不上、订单状态枚举根本走不通。原因很简单没有先想清楚每一类角色在平台上的完整操作闭环。我建议动手之前先给每个角色画一条主流程线比如用户从首页看服务到下单支付再到确认完成评价服务人员从上线到接单到开始服务到完工结算。角色画面清楚了表结构自然就出来了。1.2 极速达和普通预约型家政本质差别在哪里普通家政平台的核心是预约你今天下单、明天上门那种系统只需要把订单记录下来靠人工排班。极速达不一样它的卖点是当下响应、快速上门所以系统必须处理三件普通平台不强调的事实时在线状态服务人员不是永远在线的他可能从在线变成服务中又变成休息中。派单时只能派给真正空闲的人。基于位置的匹配用户打开 App系统要按距离把附近可用的服务人员捞出来超出服务半径的不能派。超时与追单机制派出去的订单服务人员多久不接就必须自动流转到其他人否则极速达就变成慢速达了。这三点是毕设作品能不能从C级跳到A级的关键分水岭。如果你的论文里只写用户下单后管理员后台指派人员那本质上还是 ERP不是平台。1.3 给毕设做减法哪些必须做哪些可以砍做毕设时间有限我不建议你把市面上家政 App 的功能全部复刻。这里给一份我总结的 MVP 清单按优先级排优先级功能模块说明必须做用户注册登录JWT 无状态登录必须做服务分类与列表两级分类足够必须做下单-派单-接单-完成核心状态机必须做位置匹配计算距离附近人员列表必须做支付/退款模拟余额支付 退款流水必须做评价闭环订单完成后才能评价建议做管理后台统计ECharts 图表展示可以砍在线聊天用电话代替不做 IM可以砍优惠券/会员优先级很低可以砍复杂推荐算法按距离评分排序足够按这个清单做下来项目已经有完整的业务闭环答辩时也讲得出设计逻辑而不是堆了一堆没跑通的页面。2. 数据库建模把状态机和位置匹配落到每一张表上2.1 用户与服务人员一份账号体系还是两套这是最先要定下来的问题。我的建议是一套用户表服务人员是用户的扩展角色。单独建一张user表存账号基础信息手机号、密码、昵称、头像再建一张service_worker表通过user_id和用户关联额外的字段放服务人员专属属性服务状态、经纬度、服务半径、评分、服务次数、简介。这样做的理由是服务人员本质上也是平台的注册用户将来如果要支持用户申请成为服务人员这个功能直接在service_worker表里插一条记录就是入驻审核不需要搞两套登录。service_worker表里几个必填字段我列一下user_id关联用户表主键。status在线状态建议用0-离线、1-在线、2-服务中、3-休息。派单查询时永远限定status1。latitude/longitude服务人员当前位置每次上线或开始服务前更新。service_radius服务半径单位公里默认 5。score综合评分由订单评价表聚合而来。顺带说一句经纬度字段类型用DECIMAL(10,6)千万别用VARCHAR否则后端计算距离时每次都要做一次类型转换数据量一上来就尴尬。2.2 服务类目与服务项目的两级结构服务不是简单一张表我建议拆成两级service_category类目比如日常保洁家电清洗老人陪护。service_item具体服务项目比如日常保洁下面有2小时日常保洁4小时深度保洁每个项目有起步价、单位、描述、图片。为什么要拆两级因为服务人员往往不是全能的他只擅长某个类目下的几项服务所以还需要一张worker_service关联表表示这个服务人员能做哪些项目、价格是多少。派单的时候先按照用户选择的服务项目去找拥有这个服务能力的在线人员再进行距离过滤和排序。很多毕设直接把服务项目和服务人员做成了一对多看起来简单但后面做能力匹配时一定会后悔。2.3 订单表与状态机核心中的核心订单表是整个项目的灵魂字段设计一定要克制。主表字段大概这些就够了字段说明order_no订单号建议yyyyMMddHHmmss 随机数user_id下单用户worker_id接单的服务人员item_id服务项目address服务地址冗余一次避免关联查询contact_name/phone联系人信息expected_time期望上门时间amount订单金额status状态枚举值create_time/pay_time/start_time/finish_time时间轴字段订单状态机我强烈建议用枚举常量而不是在代码里到处写String。我常用的状态枚举是这样的public enum OrderStatus { PENDING_PAY(0, 待支付), PENDING_MATCH(1, 待派单), PENDING_ACCEPT(2, 待接单), SERVICE_STARTED(3, 服务中), SERVICE_FINISHED(4, 已完成), COMMENTED(5, 已评价), CANCELED(6, 已取消), REFUNDED(7, 已退款); private final int code; private final String desc; }状态流转的方向必须提前约定清楚我画一条主干PENDING_PAY - PENDING_MATCH - PENDING_ACCEPT - SERVICE_STARTED - SERVICE_FINISHED - COMMENTED 取消分支: PENDING_PAY/ PENDING_MATCH / PENDING_ACCEPT - CANCELED 退款分支: SERVICE_FINISHED 之后可发起 - REFUNDED这里有个实际经验不要把已支付单独做成一个状态。支付完成之后直接进入PENDING_MATCH用pay_time是否为空来区分是否已经付款。这样状态总数少、流转路径短代码里判断也更爽快。2.4 支付流水、评价与地址辅助表不能省支付流水表payment_flow是资金安全的凭证记录用户支付、平台退款、服务人员提现、平台佣金抽成四类场景。字段可以统一成order_id、user_id、worker_id、amount、type支付/退款/提现/佣金、pay_method、status、create_time。毕设阶段不需要对接真实支付但这张流水表一定要建答辩时老师说你这个钱怎么对账你就把流水表甩出来逻辑是站得住的。评价表order_comment关联订单号和评分我只说一点评价状态最好由订单状态驱动订单进入COMMENTED时写评价记录别让用户先评价再改订单状态两边容易不一致。地址表user_address存用户常用地址下单选地址时直接从列表里带出来。订单表冗余一份完整地址文案避免以后用户改了默认地址导致历史订单地址漂移。3. Spring Boot 后端工程分层架构与统一约定3.1 包结构分层是为了答辩时不被问倒后端工程我习惯用这种包结构com.example.homeclean ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config ├── common │ ├── result │ ├── exception │ └── enums └── utilscontroller只做参数接收和结果返回不写业务逻辑service层写业务编排mapper层对应 MyBatis 的数据库操作。很多同学喜欢把查询逻辑直接写在 Controller 里代码短的时候很爽一旦出现下单后要扣库存、发通知、写流水这种多步骤操作Controller 里会挤满一堆临时变量根本没法维护。分层不是给老师看的是给自己降低返工成本的。3.2 统一响应、全局异常与请求拦截先做三件基础但极加印象分的事第一统一返回结果。接口不要有的返回Map、有的返回实体定义一个ResultT泛型封装Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }第二全局异常处理。把业务异常、参数校验异常、兜底异常统一拦截返回给前端的是结构一致的 JSON而不是一堆堆栈信息。前端拿到code ! 200就知道要弹错误提示。第三JWT 登录拦截。我推荐用一个拦截器统一校验请求头里的 Token再通过注解区分角色权限。比如自定义一个RequireRole(value {USER, WORKER})拦截器里从 Token 解析出用户角色做校验这样每个接口的权限声明写起来很干净也方便答辩时讲我是怎么设计权限体系的。3.3 核心 API 清单照着列就不会漏功能我整理了一份最简接口清单开发时对着它逐个实现即可用户端POST /api/user/register、POST /api/user/loginGET /api/service/category/list服务分类树GET /api/service/item/list?categoryId某分类下服务项目POST /api/order/create创建订单POST /api/order/pay模拟支付GET /api/order/detail?orderId订单详情POST /api/order/cancel取消订单POST /api/order/comment评价服务人员端POST /api/worker/online、POST /api/worker/offline上下线GET /api/worker/pendingOrders待接单列表系统推送给他/他主动刷新POST /api/worker/accept接单POST /api/worker/startService开始服务POST /api/worker/finishService完成服务GET /api/worker/settlements查看资金流水管理端POST /api/admin/worker/audit审核入驻GET /api/admin/statistics/orderCount订单量统计GET /api/admin/statistics/income平台收入统计接口有了剩下的就是往里面填逻辑。核心逻辑我在下一节详细讲。4. 派单调度与接单闭环极速达的心脏4.1 附近服务人员怎么找经纬度计算与查询查找附近可接单的服务人员最靠谱的方案是先按经纬度范围粗筛再计算距离排序。范围粗筛可以用一个简单的包围盒公式纬度每度约 111 公里经度每度距离需要乘cos(纬度)。假设要找 5 公里内的人double latDelta radius / 111.0; double lngDelta radius / (111.0 * Math.cos(userLat * Math.PI / 180));然后拼 SQL 条件latitude BETWEEN (userLat - latDelta) AND (userLat latDelta)同时限定经度范围先缩小候选集合。最后用 Haversine 公式精确计算距离SELECT w.*, ROUND(6371 * 2 * ASIN(SQRT( POWER(SIN((#{userLat} - w.latitude) * PI() / 180 / 2), 2) COS(#{userLat} * PI() / 180) * COS(w.latitude * PI() / 180) * POWER(SIN((#{userLng} - w.longitude) * PI() / 180 / 2), 2) )), 2) AS distanceKm FROM service_worker w WHERE w.status 1 AND w.latitude BETWEEN #{minLat} AND #{maxLat} AND w.longitude BETWEEN #{minLng} AND #{maxLng} AND w.service_radius 2 * ( 6371 * 2 * ASIN(SQRT( POWER(SIN((#{userLat} - w.latitude) * PI() / 180 / 2), 2) COS(#{userLat} * PI() / 180) * COS(w.latitude * PI() / 180) * POWER(SIN((#{userLng} - w.longitude) * PI() / 180 / 2), 2) )) ) ORDER BY distanceKm ASC LIMIT #{limit}你不需要背着公式但你一定要理解它的含义先粗筛把候选集合变小再精确算距离排序。如果一上来就对全表做距离计算数据量到几千条时接口响应就会明显变慢。答辩时老师如果问能不能优化你可以说增加经纬度索引 粗筛条件这就够了。4.2 自动派单还是抢单我建议做成混合模式极速达场景下用户要的是响应速度所以不能只做服务人员自己刷单。我推荐的模式是用户下单支付完成后订单进入PENDING_MATCH。系统按距离和服务能力匹配出候选服务人员列表按距离评分排序。优先把订单推送给最近的一位在线服务人员给他 60 秒确认时间。如果 60 秒内未接单自动流转给下一位候选人如果候选列表里的服务人员在两轮内都没接订单标记为派单失败退回给用户选择等待或者取消。这个流程在代码里可以用一个定时任务实现PENDING_ACCEPT状态的订单一旦超过 60 秒没有更新为SERVICE_STARTED就把worker_id置空、状态退回PENDING_MATCH重新执行一次派单逻辑。有同学问要不要做多个服务人员同时抢单的并发场景我建议是从简按顺序推送而不是并发抢单。并发抢单要引入 Redis 分布式锁或者数据库乐观锁不是不能做而是对毕设来说投入产出比不高且容易出现订单被两个人同时接了的演示事故。顺序推送逻辑清楚、演示效果稳定答辩解释起来也简单。4.3 接单状态流转的并发控制一行 update 解决问题即便做顺序推送也要防止一种极端情况上一轮被推送的服务人员一直在刷新接口、系统还没来得及超时他在超时之前点了接单——而此时订单已经被系统流转给下一个人。两个人都点了接单就脏了。解决办法是用带条件更新的乐观锁接单方法里执行这类 SQLint rows orderMapper.acceptOrder(orderId, workerId, currentStatus, new Date()); // 对应 SQL: UPDATE t_order SET worker_id #{workerId}, status #{targetStatus}, // accept_time #{now} WHERE id #{orderId} AND status #{currentStatus} if (rows 0) { throw new BizException(该订单已被接取请刷新列表); }WHERE status #{currentStatus}这个条件非常关键它保证从读状态到改状态之间如果发生了并发改动后面的更新会失败受影响行数为 0。我们只需要判断rows 0就知道抢单失败了不需要加锁也不用事务嵌套简单又可靠。4.4 服务进行中的异常环节超时、取消、改派订单被接取之后不代表就结束战斗了。有几个场景必须提前在代码里埋好处理逻辑服务人员接单后未按时上门简单做法是提供用户催单接口复杂做法是超时自动取消。毕设阶段我建议提供用户取消入口但要求服务人员开始服务前取消需要平台审核。服务中用户取消如果师傅已经上门了用户取消会导致白跑一趟。我在项目里做了服务开始后不可直接取消需联系平台相当于一个状态约束简单有效。派单失败后的订单重试不要无休止地重试最多重试两轮。两轮之后标记MATCH_FAILED用户可以申请退款也可以稍后手动再次派单。这些边界场景不需要全部做成复杂状态但你的代码里必须有对应分支。很多毕设答辩翻车就是因为老师随便问了一句用户下单后不支付怎么办师傅一直不接单怎么办学生答不上来。5. 支付、退款与结算资金流要闭环不能只做表面功夫5.1 毕设项目的三种支付方案我推荐最后一种对接真实支付渠道需要商户资质学生个人基本走不通。常见的替代方案有三种沙箱环境支付宝/微信都有沙箱可以模拟真实支付流程但配置繁琐、回调地址要内网穿透折腾成本高。第三方 Mock 支付页面自己写一个模拟收银台输入密码后跳转成功页适合演示。余额支付 后台充值给用户账户加一个balance字段提供一个模拟充值功能下单直接用余额扣减。我推荐第三种。它在表结构上完全兼容未来的真实支付接入——你该建的支付流水表一张不少只是扣款途径从第三方回调变成内部余额扣减。答辩时你完全可以这样讲我把真实支付的逻辑抽象成内部账户流水将来接入支付宝只需要新增一个渠道适配器。这句话说出来老师就知道你不是只会抄代码。5.2 先支付还是先派单这个顺序必须想清楚我见过不少项目订单创建后直接就派单用户不付款师傅也去上门了这是资金流的严重漏洞。正确的流程应该是创建订单(待支付) - 支付成功(进入待派单) - 匹配服务人员 - 接单 - 开始服务 - 完成也就是说派单动作必须发生在支付成功之后。这样既能保证师傅不会空跑也能在用户不支付时让订单自然过期不会占用派单资源。代码实现时用户点击立即支付后先调余额扣减扣减成功再写支付流水、更新订单状态为PENDING_MATCH最后触发派单逻辑。扣减余额的操作要注意并发问题直接用 SQL 原子扣减UPDATE t_user SET balance balance - #{amount} WHERE id #{userId} AND balance #{amount}如果受影响行数为 0说明余额不足或并发扣减冲突此时直接回滚不要先查后改。这是账户类操作的标准姿势。5.3 服务人员的结算与平台抽佣两张流水搞定师傅干完活钱不能直接全进他口袋平台要抽成。我在项目里的结算逻辑是用户支付时全额写入支付流水同时给平台记一笔佣金收入流水佣金比例预设 10%可以在后台配置。订单完成后系统给服务人员生成一笔可提现余额记录师傅申请提现时再生成一笔提现流水后台审核通过后把账户余额转出。为了演示方便我给service_worker表加了settlement_balance可结算余额和total_income累计收入两个字段。订单完成后由定时任务或事件触发结算settlement_balance amount * (1 - 佣金比例)同时写资金流水表。这样老师问平台怎么赚钱师傅的钱在哪看你都有明确的数据支撑而且就在页面上能展示出来。5.4 退款路径状态回滚要保证幂等退款是考核一个系统严谨性的好切口。我的退款流程如下用户发起退款仅允许在SERVICE_STARTED之前操作上课前退课这个常识类比过来。后台审核退款时先把用户余额加上退款金额再插入一条退款流水最后把订单状态从PENDING_MATCH/PENDING_ACCEPT更新为REFUNDED。关键点三步操作必须放在同一个事务里任何一个失败全部回滚。退款流水要有唯一的业务单号防止后台重复点击导致重复退款。代码层面只要记住一个词幂等。退款接口的入参里带上订单号执行前查一下退款流水表是否已有order_id的记录有就直接返回已经退款不做二次操作。这个细节写进论文里异常场景处理这一章就有东西可写了。6. 联调演示与答辩战场让项目赢在最后一公里6.1 准备一套能讲出故事感的演示数据项目都写完了千万不能在演示时临时注册账号、现场下单然后干等师傅接单页面刷新整个过程又慢又尴尬。我会提前准备一套演示剧本注册两个服务人员账号提前把位置设置在用户地址附近 2 公里内状态设为在线。准备一个用户账号余额充足比如 8888 元地址预设为某演示小区。提前录好一段演示流程用户登录 - 选日常保洁 2 小时 - 下单支付 - 服务人员端刷新看到待接单 - 接单 - 开始服务 - 完成 - 用户评价 - 看服务人员的结算余额。管理端提前准备好一张平台近 7 天订单量的统计图。演示的最大忌讳是临时造数据。我建议你把sql初始化脚本里写好一套种子数据项目启动后直接就能跑通完整流程省得每次演示前手动补数据。这也是我每次做项目分享都会强调的一点项目写得跑得通是及格带着剧本跑得顺才是加分。6.2 部署方案与演示顺序部署我推荐两种方式二选一本地演示后端mvn spring-boot:run前端npm run dev适合实验室局域网演示。云服务器部署装 MySQL、Redis、后端打成 jar 包、前端打包后由 Nginx 托管。云服务器最省心的配置是 2C4G毕设这种并发量完全足够。演示顺序我遵循用户视角先行技术深度殿后的原则先完整跑一遍用户下单流程让老师直观看到系统能用再切到服务人员端展示接单和状态流转切到管理端展示统计报表最后如果老师追问技术细节再打开数据库表结构、API 文档、关键代码讲设计。6.3 答辩追问 Top 5 与应答思路我根据经验总结出评委最常问的问题提前准备好答案现场就不慌追问问题应答思路订单状态怎么设计的为什么不用字符串用枚举常量状态流转路径明确防止非法状态。两个人同时接单怎么处理乐观锁 带条件 UPDATE受影响行数为 0 即抢单失败。支付安全怎么保证余额原子扣减 资金流水表幂等控制真实支付可平滑替换。附近服务人员是怎么排序的经纬度粗筛 Haversine 距离公式精确排序。系统有哪些亮点极速达派单闭环、资金流幂等、权限角色分离。回答时记住一个原则用代码逻辑说话不要背概念。老师问什么就打开对应的代码和表结构把 WHERE 条件、枚举定义、事务注解指给他看比背十句八股都管用。做这种 Spring Boot 项目我最大的体会是技术难度从来都不是毕设的真正门槛业务闭环才是。CRUD 页面谁都能堆出来但订单状态怎么流转、资金怎么对账、派单怎么匹配这些问题想清楚了代码自然就有结构。家政平台这个题每年都有人做但你只要把极速达这三个字背后的业务逻辑讲透了你的项目就不可能是平庸的那一个。如果你正在卡在派单或者结算这块不妨按这个思路重新捋一遍表结构和状态机应该能少走不少弯路。