☰
Spring Boot家政平台极速达核心实现:派单调度与订单资金闭环设计
2026/10/10 10:32:51 网站建设 项目流程

每年到这个时间点,都会有人来问 Spring Boot 家政服务平台这个毕设题怎么下手。说实话,这个题目每年一批人做,但大多数交上去的东西最后都长成了一个"管理员可以增删服务分类"的后台管理系统——答辩时一问到核心业务逻辑就卡壳。家政平台真正有含金量的地方从来不是 CRUD,而是标题里那"极速达"三个字:用户下单之后,系统怎么快速匹配到合适的服务人员;订单从发起到完结,状态机怎么设计才不乱;支付、退款、结算这条资金流又该怎么闭环。这篇文章我就把这整条链路拆开讲,从数据库表设计到派单调度再到资金结算,适合正在准备 springboot 毕设、尤其是想把这个题目真正做出亮点的同学参考。我尽量说实操,不给空话。

1. 极速达家政的"快"字诀:先想清楚业务边界再动键盘

1.1 三种角色,各自要的是什么

家政服务平台不是简单的"用户和服务人员一对一"关系,至少要有三类角色同时在线运转:

  • 普通用户:浏览服务、下单预约、支付、查看服务进度、评价。
  • 服务人员:接单、开始服务、完成服务、查看自己的结算流水。
  • 平台管理员:审核服务人员入驻、管理服务类目、处理异常订单、查看平台统计数据。

很多同学一上来就照着别人开源项目的表结构抄,抄完发现用户表和角色表对不上、订单状态枚举根本走不通。原因很简单:没有先想清楚每一类角色在平台上的完整操作闭环。我建议动手之前,先给每个角色画一条主流程线,比如用户从"首页看服务"到"下单支付"再到"确认完成+评价",服务人员从"上线"到"接单"到"开始服务"到"完工结算"。角色画面清楚了,表结构自然就出来了。

1.2 "极速达"和普通预约型家政,本质差别在哪里

普通家政平台的核心是"预约",你今天下单、明天上门那种,系统只需要把订单记录下来,靠人工排班。极速达不一样,它的卖点是当下响应、快速上门,所以系统必须处理三件普通平台不强调的事:

  1. 实时在线状态:服务人员不是永远在线的,他可能从"在线"变成"服务中",又变成"休息中"。派单时只能派给真正空闲的人。
  2. 基于位置的匹配:用户打开 App,系统要按距离把附近可用的服务人员捞出来,超出服务半径的不能派。
  3. 超时与追单机制:派出去的订单,服务人员多久不接就必须自动流转到其他人,否则"极速达"就变成"慢速达"了。

这三点是毕设作品能不能从"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-休息。派单查询时永远限定status=1。
  • 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 └── utils

controller只做参数接收和结果返回,不写业务逻辑;service层写业务编排;mapper层对应 MyBatis 的数据库操作。很多同学喜欢把查询逻辑直接写在 Controller 里,代码短的时候很爽,一旦出现"下单后要扣库存、发通知、写流水"这种多步骤操作,Controller 里会挤满一堆临时变量,根本没法维护。分层不是给老师看的,是给自己降低返工成本的。

3.2 统一响应、全局异常与请求拦截

先做三件基础但极加印象分的事:

第一,统一返回结果。接口不要有的返回Map、有的返回实体,定义一个Result<T>泛型封装:

@Data 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.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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/login
  • GET /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 自动派单还是抢单?我建议做成混合模式

极速达场景下,用户要的是响应速度,所以不能只做"服务人员自己刷单"。我推荐的模式是:

  1. 用户下单支付完成后,订单进入PENDING_MATCH。
  2. 系统按距离和服务能力匹配出候选服务人员列表,按距离+评分排序。
  3. 优先把订单推送给最近的一位在线服务人员,给他 60 秒确认时间。
  4. 如果 60 秒内未接单,自动流转给下一位候选人;如果候选列表里的服务人员在两轮内都没接,订单标记为"派单失败",退回给用户选择等待或者取消。

这个流程在代码里可以用一个定时任务实现:PENDING_ACCEPT状态的订单一旦超过 60 秒没有更新为SERVICE_STARTED,就把worker_id置空、状态退回PENDING_MATCH,重新执行一次派单逻辑。

有同学问要不要做"多个服务人员同时抢单"的并发场景,我建议是从简:按顺序推送,而不是并发抢单。并发抢单要引入 Redis 分布式锁或者数据库乐观锁,不是不能做,而是对毕设来说投入产出比不高,且容易出现"订单被两个人同时接了"的演示事故。顺序推送逻辑清楚、演示效果稳定,答辩解释起来也简单。

4.3 接单状态流转的并发控制:一行 update 解决问题

即便做顺序推送,也要防止一种极端情况:上一轮被推送的服务人员一直在刷新接口、系统还没来得及超时,他在超时之前点了接单——而此时订单已经被系统流转给下一个人。两个人都点了接单,就脏了。

解决办法是用带条件更新的乐观锁,接单方法里执行这类 SQL:

int 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 毕设项目的三种支付方案,我推荐最后一种

对接真实支付渠道需要商户资质,学生个人基本走不通。常见的替代方案有三种:

  1. 沙箱环境:支付宝/微信都有沙箱,可以模拟真实支付流程,但配置繁琐、回调地址要内网穿透,折腾成本高。
  2. 第三方 Mock 支付页面:自己写一个模拟收银台,输入密码后跳转成功页,适合演示。
  3. 余额支付 + 后台充值:给用户账户加一个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 页面谁都能堆出来,但订单状态怎么流转、资金怎么对账、派单怎么匹配,这些问题想清楚了,代码自然就有结构。家政平台这个题每年都有人做,但你只要把"极速达"这三个字背后的业务逻辑讲透了,你的项目就不可能是平庸的那一个。如果你正在卡在派单或者结算这块,不妨按这个思路重新捋一遍表结构和状态机,应该能少走不少弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询