☰
同城上门预约派单系统源码实战:业务模型、派单算法与状态机设计
2026/10/6 5:02:44 网站建设 项目流程

简介:这是一套面向同城上门服务行业的运营级预约派单系统源码,适合美容、家政、足浴、SPA、私教、保洁、维修等预约到家场景的开发者与创业者参考。后端采用PHP开发,前端基于uniapp实现前后端分离,可同时编译到公众号、H5、小程序与APP多端,整体架构贴近仿东郊到家的技师派单模式,便于二次开发与快速搭建自有平台。资源包共288个文件,以168个vue组件、65个js脚本、15个json配置为主,另含wxss样式、md说明文档及少量svg、png等静态资源,压缩包约764KB,目录结构清晰,便于按模块检索与阅读。目前已有2692人学习下载,说明其在同类预约系统中具备一定参考热度。读者可从中获取完整的多端页面结构、派单与预约业务逻辑、接口组织方式及前端组件拆分思路,适合具备一定PHP与uniapp基础、希望研究上门服务系统实现或进行定制开发的读者使用。

1. 同城上门预约派单:一套系统要同时扛住用户端、技师端和调度台

同城上门预约派单这类系统,表面看是「用户下单、技师接单」两件事,真正落地时你会发现它同时压着三条业务线:用户端要能选服务、选时间、选技师、付款;技师端要能抢单、改状态、上传服务记录;调度台要能派单、改派、看履约。标题里「仿东郊到家源码」这类词,本质是在说一套已经跑通的业务模型——把美容、家政、足浴、SPA 这些非标服务,塞进一个可预约、可派单、可结算的标准化流程里。

我做过几套类似的小程序加 APP 组合,最深的体会是:这类项目的难点从来不在页面画得多好看,而在「时间冲突怎么判」「技师和订单怎么匹配」「状态机怎么不打架」。新手容易把它当成一个商城来做,结果做到派单环节就翻车。这篇笔记按「业务模型 → 数据表 → 派单算法 → 状态机 → 避坑 → 进阶」的顺序讲,适合正在评估这个方向、或者已经拿到一份源码但不知道怎么改的开发者。

2. 先定业务模型:预约、派单、履约三段时间轴怎么切

2.1 为什么「预约制」和「即时单」必须分开建模

同城上门服务里,订单其实分两类。一类是即时单:用户现在就要,系统按距离和空闲状态找最近的技师。另一类是预约单:用户选明天下午三点,系统要判断这个时间段有没有技师可排。这两类的调度逻辑完全不同,如果共用一张表、一套状态,后面必然乱。

我一般会在订单主表里加一个order_type字段,取值instant和reserved。即时单走「广播抢单 + 超时改派」,预约单走「排班匹配 + 提前锁定」。这样拆开之后,技师端的接单列表、调度台的派单面板都能按类型过滤,逻辑清晰很多。

另一个关键点是服务时长。美容、SPA 这类服务,60 分钟和 90 分钟是两种排班粒度。如果排班表只按「小时」切,90 分钟的单子就会跨两个格子,导致后续排班判断出错。常见做法是把时间切成 15 分钟一个 slot,服务时长换算成 slot 数,排班和冲突判断都基于 slot 做。

2.2 技师、服务、门店三张核心表怎么设计

数据表设计决定了后面派单能不能跑起来。我一般会保留这几张核心表:

表名关键字段作用
technicianid, name, status, lat, lng, skill_tags技师基础信息与实时位置
service_itemid, name, duration_slots, price, category服务项目与时长
schedule_slotid, technician_id, date, slot_index, order_id排班与占用
order_mainid, user_id, technician_id, order_type, status, start_time订单主表
dispatch_logid, order_id, from_tech, to_tech, reason, created_at派单与改派记录

skill_tags用 JSON 或逗号分隔存技师能做的服务类别,派单时先按标签过滤,再按距离排序。schedule_slot是排班的核心,每个技师每天有固定数量的 slot,被订单占用后标记order_id,释放时清空。dispatch_log看起来可选,但实际运维时非常有用——改派纠纷、技师投诉、订单超时,全靠这张表回溯。

提示:技师位置不要只存一个经纬度,建议同时存last_active_at,超过 10 分钟没更新的位置在派单时要降权,否则会把单派给一个已经离线但状态没改的技师。

2.3 从下单到完成的完整状态流转

订单状态机是这类系统最容易出 bug 的地方。我见过一套源码,状态有pending、accepted、serving、done、cancelled,但技师端可以跳过accepted直接点serving,结果调度台看到的订单状态和实际履约完全对不上。

我的做法是把状态流转写成一张明确的迁移表,任何状态变更都必须经过校验:

# 订单状态迁移规则:key 是当前状态,value 是允许迁移到的状态集合 ORDER_TRANSITIONS = { "pending": {"accepted", "cancelled"}, # 待接单 -> 已接单 / 已取消 "accepted": {"serving", "cancelled", "pending"}, # 已接单 -> 服务中 / 取消 / 超时回退 "serving": {"done", "cancelled"}, # 服务中 -> 已完成 / 异常取消 "done": set(), # 终态 "cancelled": set(), # 终态 } def can_transition(current, target): """校验状态迁移是否合法,非法迁移直接拒绝并记录日志""" allowed = ORDER_TRANSITIONS.get(current, set()) if target not in allowed: raise ValueError(f"非法状态迁移: {current} -> {target}") return True

这段逻辑说明:pending只能到accepted或cancelled,技师不能跳过接单直接开始服务。accepted允许回退到pending,是为了处理「技师接单后超时未出发,系统自动改派」的场景。参数上,ORDER_TRANSITIONS建议放在配置中心或数据库里,方便运营调整,不要硬编码在业务代码里。

3. 派单算法落地:从广播抢单到智能匹配的取舍

3.1 广播抢单和定向派单分别适合什么场景

派单模式没有绝对优劣,关键看业务阶段。早期技师少、订单少,广播抢单最简单:订单推给附近所有符合条件的技师,谁先点谁得。优点是实现快,缺点是技师会挑单,远的、便宜的单没人接。

定向派单是系统按规则选一个技师直接派过去,适合技师多、订单密的平台。规则可以是「距离最近 + 当前空闲 + 评分最高」加权。我一般会先上广播抢单跑通流程,等订单量起来、技师覆盖密度够了,再切定向派单。切换时保留广播作为兜底——定向派单 30 秒没人接,自动转广播。

3.2 用距离、评分、空闲度做加权匹配的代码实现

定向派单的核心是一个打分函数。下面这段是我常用的简化版:

import math def score_technician(order, tech, now): """ 计算技师匹配得分,分数越高越优先派单 order: 订单信息,含 lat/lng、service_item_id tech: 技师信息,含 lat/lng、rating、last_active_at、skill_tags """ # 1. 技能匹配:不匹配直接返回 -1,排除 if order["service_item_id"] not in tech["skill_tags"]: return -1 # 2. 距离得分:用 Haversine 算直线距离,5 公里内线性衰减 dist_km = haversine(order["lat"], order["lng"], tech["lat"], tech["lng"]) if dist_km > 10: return -1 # 超过 10 公里不派 dist_score = max(0, 1 - dist_km / 10) * 40 # 距离权重 40 # 3. 评分得分:5 分制,归一化后乘权重 30 rating_score = (tech["rating"] / 5.0) * 30 # 4. 活跃度得分:最近 5 分钟活跃满分,超过 10 分钟为 0 idle_seconds = (now - tech["last_active_at"]).total_seconds() active_score = max(0, 1 - idle_seconds / 600) * 30 return dist_score + rating_score + active_score def haversine(lat1, lng1, lat2, lng2): """计算两个经纬度之间的直线距离,单位公里""" R = 6371 dlat = math.radians(lat2 - lat1) dlng = math.radians(lng2 - lng1) a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * \ math.cos(math.radians(lat2)) * math.sin(dlng/2)**2 return R * 2 * math.asin(math.sqrt(a))

逻辑说明:先做硬性过滤(技能不匹配、距离超限直接排除),再对剩下的技师做加权打分。距离权重 40、评分 30、活跃度 30,这三个数不是固定的,订单高峰期可以把距离权重调高,让技师少跑路;平峰期可以把评分权重调高,优先给好评技师派单。参数调整建议做成后台可配置,不要写死在代码里。

3.3 排班冲突检测:预约单最容易出错的一步

预约单派单前必须检查技师在目标时间段是否空闲。假设服务时长是 90 分钟,换算成 6 个 15 分钟 slot,那么要检查这 6 个 slot 是否都被占用。

-- 查询某技师在指定日期、指定 slot 区间内是否已被占用 SELECT COUNT(*) AS occupied FROM schedule_slot WHERE technician_id = ? AND date = ? AND slot_index BETWEEN ? AND ? AND order_id IS NOT NULL;

如果occupied大于 0,说明有冲突,不能派单。这里有个坑:预约单锁定 slot 的时机。我一般是在用户支付成功后才锁定,而不是下单就锁。否则用户下单不付款,slot 被占着,其他用户约不了,技师也接不了别的单。支付成功后锁定,锁定失败则退款并提示改约。

4. 避坑与排查:派单系统上线后最常炸的五个地方

4.1 技师位置更新不及时导致派单距离失真

现象:调度台显示技师就在用户附近,派单后技师说「我离那边还有 8 公里」。原因:技师端 APP 的位置上报频率太低,或者切到后台后停止上报,数据库里存的是旧位置。解决:技师端在前台时每 30 秒上报一次,接单状态下每 10 秒上报一次;后端在派单打分时,对last_active_at超过 5 分钟的记录做降权或直接排除。

4.2 订单状态被并发操作改乱

现象:用户取消订单的同时技师点了接单,结果订单既显示「已取消」又显示「已接单」。原因:状态变更没有加锁,两个请求同时读到pending,各自改成不同状态。解决:状态变更用数据库乐观锁,UPDATE order_main SET status = ? WHERE id = ? AND status = ?,根据影响行数判断是否成功。失败的一方返回「订单状态已变更,请刷新」。

4.3 预约单跨天排班算错 slot

现象:用户约了晚上 11 点半的 90 分钟服务,系统排班时只检查了当天 slot,结果技师实际服务到第二天凌晨,第二天早上的排班冲突了。原因:slot 计算没有跨天处理。解决:把日期和 slot 合并成一个连续的时间轴索引,比如day_index * 96 + slot_index(每天 96 个 15 分钟 slot),跨天时自然延续到下一个 day_index。

4.4 改派后原技师仍能看到订单

现象:订单改派给新技师后,原技师端列表里还能看到这个单,点进去还能操作。原因:技师端列表缓存没有失效,或者查询条件只按状态过滤没按technician_id过滤。解决:改派时写入dispatch_log,同时给原技师端推送一条「订单已改派」的消息,客户端收到后从列表移除;服务端查询强制带technician_id = 当前技师。

4.5 服务完成后结算金额和预期不符

现象:技师完成服务,结算时发现平台抽成算错了。原因:抽成规则可能按服务类别、技师等级、活动折扣不同而不同,如果结算时只用一个固定比例,就会出错。解决:把抽成规则做成配置表,按service_item_id或category维度配置,结算时先查规则再计算。所有结算记录写流水表,方便对账。

5. 进阶技巧:用调度日志反推派单策略该怎么调

5.1 从 dispatch_log 里读出三个关键指标

派单策略调优不能靠拍脑袋。我一般会从dispatch_log里算三个指标:平均派单时长(从订单创建到技师接单的时间)、改派率(改派次数 / 总订单数)、技师接单率(接单数 / 被派单数)。这三个指标能直接告诉你策略哪里有问题。

-- 按天统计派单核心指标 SELECT DATE(created_at) AS stat_date, AVG(TIMESTAMPDIFF(SECOND, created_at, accepted_at)) AS avg_dispatch_seconds, SUM(CASE WHEN from_tech IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*) AS reassign_rate, SUM(CASE WHEN accepted_at IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*) AS accept_rate FROM dispatch_log GROUP BY DATE(created_at) ORDER BY stat_date DESC;

如果改派率超过 15%,说明定向派单的匹配规则太激进,或者技师活跃度数据不准。如果平均派单时长超过 3 分钟,说明广播范围太小或者在线技师太少。这些数据每天看一次,比等用户投诉再改要主动得多。

5.2 用 A/B 测试验证权重调整效果

调整派单权重时,不要一次性全量上线。我一般会把订单随机分成两组,A 组用旧权重,B 组用新权重,跑一周后对比改派率和接单率。如果 B 组改派率明显下降且接单率没掉,再全量切换。这个做法听起来简单,但很多团队嫌麻烦直接全量改,结果出了问题连回滚依据都没有。

5.3 技师端「后悔药」:接单后短时间内允许无责取消

技师接单后可能发现距离太远或者时间冲突,如果没有取消入口,他可能会硬着头皮接然后迟到,用户体验更差。我的做法是给技师一个「接单后 2 分钟内可无责取消」的窗口,取消后订单自动回到派单池。这个窗口期要记录在dispatch_log里,如果某个技师频繁使用,运营可以介入沟通。这个设计看起来是让步,实际是减少履约事故的有效手段。

最后说个我自己的习惯:每次改派单逻辑之前,先把dispatch_log导出一份最近 7 天的数据,在本地跑一遍新规则,看看如果当时用新规则会派给谁、改派率会变成多少。这个「离线回放」能挡掉大部分想当然的改动。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询