上门家政的核心运营环节是派单。和实物电商的“发货”不同,上门服务的派单涉及地理围栏、师傅运力、服务时段、改派兜底等一系列复杂逻辑。很多做家政系统的团队,一开始把派单做成“简单分配”,结果上线后问题百出:跨区域派单、师傅接不到订单、改派混乱、用户投诉。
LikeShop 上门家政 Java 版的师傅派单模块,是作为核心运营链路设计的:区域分层匹配、多通道订单推送、超时自动改派、全程状态留痕。这篇文章从架构、区域匹配、推送机制、改派逻辑四个层面拆解实现思路,附核心代码片段。
一、派单整体架构
派单不是单点操作,是一条完整的状态链路。上门家政场景下,派单分为两种模式:自动派单(系统默认,按规则匹配推送)和手动改派(运营人工介入,异常场景兜底)。
1.1 完整派单链路
用户下单支付
↓
地址解析 → 区域校验
↓
匹配可用师傅池(区域+类目+状态+负载)
↓
生成派单任务 → 多通道推送师傅
↓
┌─ 师傅接单 → 更新状态 → 上门服务 → 完成确认
│
└─ 拒单/超时未接 → 自动改派 → 重新匹配 → 推送下一位
1.2 订单派单状态机
public enum DispatchStatus {
PENDING(10, "待派单"),
PUSHED(20, "已推送"),
ACCEPTED(30, "师傅已接单"),
SERVICING(40, "服务中"),
FINISHED(50, "已完成"),
REDISPATCH(60, "改派中"),
CANCELLED(70, "已取消");
}
整个派单流程围绕这个状态机流转,所有操作前置校验状态,不允许非法跳转。LikeShop 上门家政版的设计思路是:能自动的不人工,能规则化的不例外,把绝大多数常规订单交给系统自动处理,运营只处理异常场景。
二、区域匹配:派单精准的基础
派单准不准,第一步就在区域。家政服务的半径属性很强,跨街道、跨商圈派单,师傅通勤成本高、用户等待时间长,投诉率会直线上升。
2.1 服务区域的层级设计
采用行政区域 + 自定义电子围栏双层设计:
- 行政区域层:省-市-区-街道四级,基础匹配用,适合标准化城市服务;
- 电子围栏层:自定义多边形围栏(小区、商圈、产业园),精细匹配用,适合高密度区域。
师傅可以绑定多个服务区域,同时设置最远服务半径,双重校验避免超远派单。
2.2 核心数据库表
-- 服务区域表
CREATE TABLE ls_service_area (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
city_id BIGINT NOT NULL,
area_name VARCHAR(64) NOT NULL,
area_type TINYINT NOT NULL COMMENT '1行政区域 2电子围栏',
boundary TEXT COMMENT '电子围栏多边形坐标JSON',
center_lng DECIMAL(10,6),
center_lat DECIMAL(10,6),
status TINYINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL
);
-- 师傅服务区域绑定
CREATE TABLE ls_worker_area (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
worker_id BIGINT NOT NULL,
area_id BIGINT NOT NULL,
priority INT NOT NULL DEFAULT 0 COMMENT '区域优先级',
create_time DATETIME NOT NULL,
UNIQUE KEY uk_worker_area(worker_id, area_id)
);
-- 师傅服务状态
CREATE TABLE ls_worker_status (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
worker_id BIGINT NOT NULL UNIQUE,
online_status TINYINT NOT NULL COMMENT '1在线 2忙碌 3离线',
current_area_id BIGINT COMMENT '当前所在区域',
today_order_count INT NOT NULL DEFAULT 0,
last_active_time DATETIME,
update_time DATETIME NOT NULL
);
2.3 区域匹配核心逻辑
匹配采用层级降级 + 多条件筛选:优先精细围栏匹配,没有则逐步降级到行政区域;同时校验师傅的在线状态、服务类目、当日负载。
@Service
public class WorkerMatchService {
@Autowired
private ServiceAreaMapper areaMapper;
@Autowired
private WorkerStatusMapper statusMapper;
/**
* 匹配可用师傅列表(按优先级排序)
*/
public List<Long> matchWorkers(ServiceOrder order) {
// 1. 解析订单地址,匹配所属区域
Long areaId = matchArea(order.getAddress(), order.getLng(), order.getLat());
if (areaId == null) {
throw new BizException("该地址暂不提供服务");
}
// 2. 查询区域内所有绑定的师傅
List<Long> workerIds = workerAreaMapper.getWorkerIdsByArea(areaId);
if (CollectionUtils.isEmpty(workerIds)) {
// 降级:向上级区域匹配
areaId = getParentArea(areaId);
workerIds = workerAreaMapper.getWorkerIdsByArea(areaId);
}
if (CollectionUtils.isEmpty(workerIds)) {
throw new BizException("当前区域暂无可用师傅");
}
// 3. 多条件筛选:在线、服务类目匹配、当日负载
List<WorkerStatus> availableWorkers = statusMapper.selectAvailable(
workerIds, order.getServiceId());
// 4. 排序:评分优先 > 今日单量少优先 > 距离优先
availableWorkers.sort((a, b) -> {
int scoreCompare = b.getScore().compareTo(a.getScore());
if (scoreCompare != 0) return scoreCompare;
int countCompare = a.getTodayOrderCount() - b.getTodayOrderCount();
if (countCompare != 0) return countCompare;
return getDistance(order, a).compareTo(getDistance(order, b));
});
return availableWorkers.stream()
.map(WorkerStatus::getWorkerId)
.collect(Collectors.toList());
}
private Long matchArea(String address, BigDecimal lng, BigDecimal lat) {
// 优先电子围栏匹配(点在多边形内判断)
List<ServiceArea> fences = areaMapper.selectFences();
for (ServiceArea fence : fences) {
if (GeoUtils.isPointInPolygon(lng, lat, fence.getBoundary())) {
return fence.getId();
}
}
// 兜底行政区域匹配
return areaMapper.getByAddress(address);
}
}
匹配结果是一个有序的师傅列表,派单时按顺序推送,保证最优质的师傅优先接到订单。
三、订单推送:多通道与状态回执
匹配到师傅池之后,就是推送环节。推送的核心要求是:不丢单、不重复、有回执、可超时。
3.1 两种推送策略
| 策略 | 逻辑 | 适用场景 |
|---|---|---|
| 顺序派单 | 按优先级逐个推送,一位超时未接则推下一位 | 订单充足、师傅多,保证优质师傅优先 |
| 群派模式 | 同时推送给多位师傅,先抢先得 | 订单稀缺、高峰期,提高接单率 |
默认采用顺序派单,兼顾公平和效率;高峰期可配置切换成群派。
3.2 多通道推送矩阵
不是只靠 APP 推送,要做分层通道,保证触达率:
- APP 推送:师傅在线时优先,实时性最高,免费;
- 短信通知:师傅离线时兜底,带接单链接;
- 语音电话:高价值订单、紧急订单,强提醒。
3.3 推送核心实现
@Service
public class OrderDispatchService {
@Autowired
private WorkerMatchService matchService;
@Autowired
private DispatchTaskMapper taskMapper;
@Autowired
private PushService pushService;
@Autowired
private AlertService alertService;
/**
* 订单支付成功,触发自动派单
*/
@EventListener
@Transactional(rollbackFor = Exception.class)
public void onOrderPaid(ServiceOrderPaidEvent event) {
ServiceOrder order = event.getOrder();
// 1. 匹配师傅列表
List<Long> workerIds = matchService.matchWorkers(order);
// 2. 生成派单任务
DispatchTask task = new DispatchTask();
task.setOrderId(order.getId());
task.setWorkerIds(JSON.toJSONString(workerIds));
task.setCurrentIndex(0);
task.setStatus(DispatchStatus.PENDING.getCode());
taskMapper.insert(task);
// 3. 推送第一位师傅
pushToWorker(task, workerIds.get(0));
}
private void pushToWorker(DispatchTask task, Long workerId) {
task.setStatus(DispatchStatus.PUSHED.getCode());
task.setCurrentWorkerId(workerId);
task.setPushTime(new Date());
taskMapper.updateById(task);
// 多通道推送
boolean pushOk = pushService.pushWorker(workerId,
PushType.NEW_ORDER, task.getOrderId());
if (!pushOk) {
// APP推送失败,短信兜底
pushService.sendSms(workerId, PushType.NEW_ORDER, task.getOrderId());
}
// 注册超时监听:15分钟未接单自动改派
DispatchTimeoutJob.register(task.getId(), 15, TimeUnit.MINUTES);
}
/**
* 师傅接单回调
*/
public void acceptOrder(Long taskId, Long workerId) {
DispatchTask task = taskMapper.selectById(taskId);
if (task.getStatus() != DispatchStatus.PUSHED.getCode()) {
throw new BizException("订单状态不可接单");
}
if (!task.getCurrentWorkerId().equals(workerId)) {
throw new BizException("非当前派单师傅");
}
task.setStatus(DispatchStatus.ACCEPTED.getCode());
task.setAcceptTime(new Date());
taskMapper.updateById(task);
// 更新师傅状态、今日单量
workerStatusService.incOrderCount(workerId);
// 通知用户:师傅已接单
notifyService.notifyUserOrderAccept(task.getOrderId(), workerId);
}
}
3.4 推送回执与幂等
所有推送都要有回执,不能“发出去就当成功了”。师傅端收到推送后,要回传接收确认;接单操作要做幂等校验,避免重复点击导致状态错乱。
四、改派逻辑:异常场景的兜底
派单不可能 100% 一次成功,拒单、超时、师傅临时有事、用户改约,都会触发改派。改派是派单系统的兜底能力,也是最容易出乱子的环节。
4.1 改派触发场景
- 师傅主动拒单:师傅原因无法服务,填写拒单原因;
- 超时未接单:推送后 15 分钟未操作,系统自动改派;
- 运营人工改派:用户改时间/改地址、特殊需求,运营介入调整;
- 师傅报备休息:师傅临时下线,未完成订单自动改派。
4.2 改派核心流程
- 回收原任务:标记原派单任务为改派中,原师傅释放订单;
- 校验改派次数:单订单最多改派 2~3 次,避免无限循环;
- 重新匹配:排除原师傅,重新匹配师傅池;
- 二次推送:按顺序推送给下一位师傅;
- 通知用户:改派后同步通知用户新师傅信息,避免投诉。
4.3 改派核心代码
@Service
public class OrderRedispatchService {
@Autowired
private DispatchTaskMapper taskMapper;
@Autowired
private WorkerMatchService matchService;
@Autowired
private DispatchLogMapper logMapper;
/**
* 改派订单
*/
@Transactional(rollbackFor = Exception.class)
public void redispatch(Long taskId, String reason, Long operatorId) {
DispatchTask task = taskMapper.selectById(taskId);
// 1. 状态校验
if (task.getStatus() != DispatchStatus.PUSHED
&& task.getStatus() != DispatchStatus.ACCEPTED) {
throw new BizException("当前状态不可改派");
}
// 2. 校验改派次数
if (task.getRedispatchCount() >= 2) {
throw new BizException("订单改派次数已达上限,请人工处理");
}
// 3. 释放原师傅
Long oldWorkerId = task.getCurrentWorkerId();
workerStatusService.decOrderCount(oldWorkerId);
// 4. 重新匹配师傅(排除原师傅)
List<Long> workerIds = matchService.matchWorkers(
getOrderByTask(taskId));
workerIds.remove(oldWorkerId);
if (CollectionUtils.isEmpty(workerIds)) {
// 无可用师傅,转人工处理
task.setStatus(DispatchStatus.PENDING);
alertService.send("派单异常", "订单" + task.getOrderId() + " 改派无可用师傅");
return;
}
// 5. 更新任务,推送给下一位
task.setRedispatchCount(task.getRedispatchCount() + 1);
task.setWorkerIds(JSON.toJSONString(workerIds));
task.setCurrentIndex(0);
taskMapper.updateById(task);
// 6. 写改派日志
DispatchLog log = new DispatchLog();
log.setTaskId(taskId);
log.setFromWorker(oldWorkerId);
log.setToWorker(workerIds.get(0));
log.setReason(reason);
log.setOperatorType(operatorId == null ? 1 : 2);
log.setOperatorId(operatorId);
log.setCreateTime(new Date());
logMapper.insert(log);
// 7. 推送新师傅
dispatchService.pushToWorker(task, workerIds.get(0));
// 8. 通知用户
notifyService.notifyUserRedispatch(task.getOrderId());
}
/**
* 超时未接单自动改派
*/
public void onTimeout(Long taskId) {
DispatchTask task = taskMapper.selectById(taskId);
if (task.getStatus() == DispatchStatus.PUSHED) {
redispatch(taskId, "超时未接单", null);
}
}
}
每次改派都留痕,谁发起的、什么原因、从哪个师傅改到哪个师傅,全部可追溯。
五、常见坑与最佳实践
- 区域重叠导致重复派单
坑:电子围栏和行政区域重叠,同一个师傅匹配到两次,重复推送。
最佳实践:匹配结果按师傅 ID 去重,同一订单同一师傅只推一次。 - 师傅在线状态失真
坑:师傅 APP 后台挂着,人其实不在,订单推送过去石沉大海。
最佳实践:加心跳机制,5 分钟无动作自动置为离线,匹配时只选在线师傅。 - 推送无回执,重复派单
坑:推送没收到回执,系统以为没发成功,重复推送,师傅收到好几次。
最佳实践:推送有回执,接单操作幂等,按任务维度去重。 - 改派不通知用户
坑:师傅换了用户不知道,上门了才发现换人,体验很差。
最佳实践:改派成功立刻推送用户通知,附新师傅信息和联系电话。 - 无负载均衡
坑:评分高的师傅单量爆炸,新师傅没单接,运力不均。
最佳实践:排序加入当日单量因子,单量少的师傅优先级提升,均衡负载。
结语
上门家政的派单,看起来是个简单的分配功能,本质是地理、运力、时效的综合调度。区域准不准、推送快不快、改派顺不顺,直接影响师傅效率和用户体验。
LikeShop 上门家政 Java 版把这套逻辑做成了标准化模块,区域围栏可配置、推送策略可调、改派有兜底、全程留痕可审计,商家不用从零踩坑。对做上门服务的商家来说,派单这条链路跑顺了,整体运营效率和服务质量都能上一个台阶。