做服务调度类项目,最让人头大的从来不是页面能做得有多花哨,而是“这件事到底该派给谁、怎么快速判断那个人现在真能用”这一连串业务规则背后的逻辑。最近我负责了一个基于React Native鸿蒙跨平台实现的调度分配模块,核心需求很明确:对候选资源做可用性筛选,再按距离最优原则给出分配建议,并且系统要同时支持人工干预和智能分配两种模式。这套模块最终跑在了跨平台移动端上,业务侧的派单准确率和响应速度都有明显提升。
这篇文章我会把整个项目的设计思路、算法选型、实操细节和踩坑过程完整梳理一遍。项目里涉及的场景我做了脱敏处理,但核心代码逻辑和技术方案都是可以直接复用的,适合正在做派单、巡检、上门服务、维修工单分配这类需求的朋友参考。
1. 项目整体设计与思路拆解
1.1 需求定位:这个模块到底解决了什么问题
先说业务背景。项目模拟的是一个多区域服务调度平台:每天有大量工单产生,每个工单需要分派给合适的服务人员,而服务人员分布在不同的地理位置,具备不同的技能标签,当前的工作状态也各不相同。过去靠人工在群里喊、在表格里查,效率低不说,还经常出现“人就在附近但被派给了远处的人”这种离谱情况。
所以这个模块的本质,是把“人找活”变成“活匹配人”。但匹配不能瞎匹配,必须满足三个约束条件:
- 这个人当前是可用的,不能被其他任务占用,也不能在非工作时段;
- 这个人具备处理这个工单所需的能力标签;
- 在满足前两条的人里,优先选择距离最近的那个,同时兼顾工作负载的均衡。
这三个约束条件,对应到系统里就是两个核心算法模块:可用性筛选和距离最优计算。而“人工干预”和“智能分配”两种模式,则分别对应两套运行逻辑,既要保证自动化运转的效率,又要给调度员留出人工判断的空间。
1.2 为什么选React Native加鸿蒙这套组合
移动端跨平台方案其实有很多选择,但这个项目有它的特殊性:终端设备一部分是现有安卓机,另一部分是新采购的鸿蒙设备,团队里又积累了比较多的前端和React技术栈经验。综合考虑下来,选择了React Native配合鸿蒙适配层来做跨平台。
为什么不用纯原生开发?因为两套原生代码的维护成本太高,业务逻辑迭代快,不可能每次改动都同步写两份。为什么不用Flutter?团队对Dart不熟,而且现有大量业务组件都是基于React生态的,迁移成本大。
React Native在鸿蒙上的适配,目前主要走的是社区和厂商联合维护的OpenHarmony适配路线。简单理解就是通过桥接层把RN的JavaScript引擎和鸿蒙的ArkUI能力打通,JS侧写业务逻辑,原生侧提供地图、定位、通知等系统能力调用。实测下来,基础组件渲染、Touch事件、网络请求这些核心能力都稳定可用。
1.3 双模式机制的设计取舍
这个项目最值得一提的设计点,是“人工干预”和“智能分配”的并存。一开始也犹豫过:既然有了算法,为什么还要人工介入?这不是多此一举吗?
实际做过调度系统的人应该都懂,完全交算法、不让人说话的系统,基本在业务侧是推不下去的。原因很简单:算法能处理常规情况,但处理不了“例外”。比如某个客户点名要老张师傅过去,比如某个区域临时有交通管制,这些信息不会及时反映在数据里,必须给调度员一个手动覆盖或者手动跳过的入口。
所以在设计上,我做了这样的划分:
- 智能分配模式下,系统自动跑筛选和距离排序,把最优分配结果直接推送下去,适合量大、常规的日常工单;
- 人工干预模式下,系统只做候选推荐,把TopN列表和推荐理由展示给调度员,由调度员确认或修改后再下发,适合需要权衡特殊因素的工单。
两种模式共用同一套筛选和排序核心,只是在最终是否自动执行这一层做了分流。这个设计的好处是,业务方可以先在人工干预模式下跑一段时间,观察推荐结果的合理性,觉得稳定了再切换成智能分配,上线风险小很多。
2. 可用性筛选与距离最优原则的实现原理
2.1 可用性筛选:状态机、技能标签和时间窗口
可用性这个词听起来简单,真落到代码里要处理的信息维度其实不少。我把它拆成了四个子条件,每个子条件都对应一套判断逻辑:
第一个是状态判断。服务人员的状态包括离线、空闲、忙碌、休假等,只有当状态为空闲时才算初步可分配。这里要注意不能只存一个布尔值,因为“忙碌”可能是有截止时间的,比如当前工单还有一个小时结束,那理论上一个小时后这个人又可以接单了。所以我给每个资源维护了一个状态机,并记录状态预期恢复时间。
第二个是技能标签匹配。每个工单有一个技能要求列表,比如需要“电工证+高空作业证”,那候选资源必须同时包含这两个标签才能进入推荐池。这个判断用集合运算就能搞定,关键在于标签体系要提前做好标准化,否则“电工”和“电工证”这种写法会导致匹配结果不稳定。
第三个是时间窗口校验。服务人员可以设置自己的工作时段,比如有些人只接上午的单,有些人周末不排班。筛选时要拿工单的期望服务时间跟资源的工作时段做交集判断,不满足的直接淘汰。
第四个是并发上限控制。为了避免给一个人同时派多个工单,我会给每个空闲资源加一个“最大并发任务数”,默认是1,但考虑到有些轻量任务可以顺路处理,也支持配置大于1的值。
这四个条件合在一起,就构成了可用性筛选的完整过滤器。实际操作中我会把筛选逻辑写成一条过滤链,每个环节只做自己该做的事,这样任何一个规则发生变化,都不需要改动其他环节。
2.2 距离最优原则:从Haversine到实操中的混合策略
距离最优是整个系统的另一个核心。距离算得准不准,直接影响推荐的合理性。
最简单的方案是用Haversine公式计算两个经纬度坐标之间的球面距离,公式本身不复杂:
function haversineDistance( lat1: number, lng1: number, lat2: number, lng2: number ): number { const R = 6371; // 地球半径,单位公里 const toRad = (deg: number) => (deg * Math.PI) / 180; const dLat = toRad(lat2 - lat1); const dLng = toRad(lng2 - lng1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }这个方案算的是“直线距离”,在服务半径比较小、路网比较规则的区域,参考价值还是不错的,而且计算速度快,适合在筛选阶段做粗排。
但实操中我很快就发现,只算直线距离远远不够。很多区域中间隔了一条河,直线距离两公里,实际绕路要走八公里。所以最终方案是“Haversine粗筛 + 地图路径距离精算”的混合策略:
- 先用Haversine把所有候选资源按距离从小到大排序,取前N个进入精算环节,这个N我一般设为10;
- 对这N个候选,调用地图服务的路径规划接口,获取真实的驾车或步行距离;
- 用真实距离重新排序,得到最终的距离最优推荐。
为什么不让所有候选资源都走路径规划接口?因为地图服务接口有配额限制,而且响应时间不稳定,如果每次分配都对几百个候选资源做路径规划,那系统延迟会变得不可接受。粗筛加精算的组合,是在精度和性能之间最实用的折中。
2.3 数据流与状态同步设计
筛选和排序都依赖一个前提——资源数据是新鲜且一致的。这里我踩过一个大坑,后面会在排查章节详细说,先讲一下正确做法。
资源位置和状态这两类数据都是高频变化的,位置可能每几秒就变,状态则受工单流转影响。我在系统设计上采用了“客户端定时上报 + 服务端缓存聚合 + 分配时实时校验”三层结构:
- 移动端每30秒上报一次定位信息,定位权限异常时退化为手动选择区域;
- 服务端把资源位置和状态存在缓存里,设置合理的过期时间,避免读到僵尸数据;
- 分配动作发生时,不是直接信任缓存,而是对最终推荐名单做一次实时状态回查,确保没有把已经被其他任务占用的资源分配出去。
这个三层结构能覆盖绝大多数情况,既保证了列表展示的流畅性,又保证了分配决策那一刻的数据准确性。
3. 实操过程:两种模式的核心实现
3.1 智能分配模式的完整链路
智能分配模式的代码逻辑是整条链路的主干。实际实现中,我给分配引擎定义了一个入参对象和返回结果结构:
interface AssignRequest { orderId: string; requiredSkills: string[]; location: { lat: number; lng: number }; expectedTime: string; mode: 'auto' | 'manual'; } interface AssignResult { candidates: Candidate[]; assignedResourceId?: string; strategy: string; }智能模式下的执行顺序是这样的:
- 根据工单的技能要求和工作时段,从全量资源池里做第一轮粗筛,这一步直接过滤掉八成以上的无效候选;
- 对粗筛通过的资源做Haversine距离计算,按距离升序排序;
- 取前10个候选做路径精算,换算成真实距离;
- 引入负载均衡因子,对排序分数做一次修正,公式是
score = realDistance + workloadWeight * (当前并发数 / 最大并发数),避免距离最近的人永远被派单、活活累死,而稍远一点的人一直闲着; - 取修正后分数最小的资源为最终分配结果,写分配流水并下发工单通知。
代码里我想强调一个细节:排序时不要把距离和时间混在一起算权重,除非你有完整的标定数据。我在第一版里试过给“预计耗时=距离/平均速度”加权,结果因为不同区域的交通状况差异太大,权重怎么调都不对,最后还是改成了“距离为主、工作量微调”的简单方案。
3.2 人工干预模式的交互细节
人工干预模式不是简单地把自动结果弹出来让人确认,那样人工就变成了流程上的摆设。真正好用的干预模式,一定要给人足够的决策依据和操作空间。
我在移动端实现了一个干预面板,核心内容有三块:
第一块是候选人推荐列表。列表按综合分数从低到高展示前5名,每项包含姓名、距离、预计耗时、当前负载、技能匹配度这五个关键指标。
第二块是推荐理由标签。每个候选人都带一两个简短标签,比如“距离最近”“负载较低”“指定偏好”,让调度员不用逐个对比数据就能快速理解推荐逻辑。
第三块是操作区。调度员可以选择直接指派某个人,也可以选择“暂不分配、备注原因”。备注原因不是可有可无的功能,它会进入分配流水,后续查问题时有据可依。
这个模式跑了一个月后,业务侧反向给我提了个需求:希望在干预面板上能看到候选人最近一个工单的完成时间,用来判断这个人现在是不是真的在赶路还是已经收工了。后来我在候选列表里加了一个“最近工单状态”字段,数据的实时性明显变好了。
3.3 鸿蒙端适配的几个关键配置
React Native的代码在鸿蒙上运行,表面上看是“复制粘贴换个壳”,实际要处理的适配问题一堆。
先说导航和地图模块。鸿蒙端没有直接可用的第三方地图SDK,我的处理方式是封装一个统一的地图接口,安卓端走原有的地图SDK,鸿蒙端走系统级定位服务加WebView地图展示兜底,业务层完全感知不到差异。
再说定位权限。鸿蒙对定位权限的申请时机和弹窗文案有自己的一套要求,需要在原生侧做适配。具体来说,我写了一个原生定位模块,通过TurboModule的方式暴露给JS侧调用,返回统一的经纬度对象。
// 原生模块暴露给RN侧的接口 import { NativeModules } from 'react-native'; interface LocationModule { getCurrentPosition(): Promise<{ latitude: number; longitude: number }>; } const { LocationModule } = NativeModules;还有一个容易忽略的问题是字体和布局适配。鸿蒙系统的默认字体渲染和安卓有细微差异,极端情况下会导致文本截断。我的经验是,在关键文本节点上不要依赖默认行高,至少设置一个最小高度属性,避免底部按钮上的文字被切掉。
4. 常见问题与排查技巧实录
4.1 鸿蒙适配里的三个典型坑
适配过程中遇到的问题不少,挑三个最有代表性的说。
第一个是图片资源路径解析异常。安卓端能正常显示的本地产图片,在鸿蒙端偶尔会报找不到资源。查了很久才发现是打包脚本对资源目录的大小写处理不一致导致的。解决方案是放弃本地图片的复杂目录结构,改成统一放入一个扁平目录,文件名全部用小写加下划线。这个改动虽然粗暴,但之后再也没有出过资源找不到的问题。
第二个是定位返回空值。部分鸿蒙设备在首次安装应用后,如果用户没有主动触发定位权限弹窗,定位模块会静默返回空对象。我加了一个“权限预检 + 失败重试”逻辑,在进入分配页面前就检查权限状态,如果未授权则弹出去设置页的引导。
第三个是地图组件层叠遮挡。RN的Modal弹层在鸿蒙端层级异常,导致弹层下方的地图触摸事件穿透。这个问题的解法是给Modal内的根容器设置一个elevation样式值,把弹层的渲染层级抬高。
4.2 距离计算性能瓶颈与优化
发布后有一段时间,智能分配接口的响应时间涨到了三秒以上,根本原因是候选资源数量级上来后,路径精算环节没有控制好并发量。
一开始我的逻辑是:粗筛后的10个候选,逐个同步调用地图路径规划接口,一个返回后才能请求下一个,串行消耗了太多时间。后来改成“并行请求 + 熔断降级”:
- 10个候选的路径规划请求同时发出,用
Promise.all等待全部返回; - 设置单次请求超时为1.5秒,超时的候选降级使用Haversine直线距离;
- 如果连续3次出现超过3个候选超时,则整个分配流程自动降级为纯Haversine模式,不再调用路径规划接口。
这个改造做完后,接口响应时间稳定在800毫秒以内,最坏情况也不会超过2秒。
顺带说一句,路径规划接口的配额消耗也要留意。我加了本地缓存,同一个起点和终点组合在10分钟内只请求一次,这个缓存命中率相当高,接口配额的压力小了很多。
4.3 双模式切换导致的状态冲突
最隐蔽的问题出现在人工干预和智能分配共享同一套状态数据的时候。
现象是这样的:调度员在人工模式下看到候选人A状态为空闲,于是准备指派;但就在他按下确认的这几十秒里,系统自动任务已经给A分配了一个在线工单,A的状态变成了忙碌。这时候人工确认再下发,就产生了任务超配。
排查后发现,问题根源在于人工干预的确认操作没有重新校验资源状态。
解法分两步:一是在客户端下发前增加一次轻量校验接口,确认资源仍处于空闲状态;二是服务端在写入分配结果时加一个乐观锁,用版本号比较,版本不一致就拒绝写入并提示调度员刷新列表。
处理完之后,超配问题基本消失了。这个经验也说明了一个问题:任何人工操作的入口,都不能跳过最终状态校验,界面展示的数据只能当作参考快照,不能当作决策依据。
4.4 问题排查速查表
| 现象 | 可能原因 | 排查顺序 | 解决方法 |
|---|---|---|---|
| 鸿蒙端图片不显示 | 资源路径大小写不一致 | 先看日志有无资源加载失败 | 统一扁平目录,小写命名 |
| 定位返回空 | 权限未预检 | 检查权限状态接口返回 | 增加权限预检与引导 |
| 智能分配响应超3秒 | 路径规划串行请求 | 看接口调用耗时分布 | 并行请求加降级缓存 |
| 人工确认后任务超配 | 状态校验缺失 | 查分配流水时间线 | 增加二次校验和乐观锁 |
| Modal遮挡地图 | 渲染层级异常 | 在鸿蒙端复现点击事件 | 设置elevation抬升层级 |
| 距离异常偏大 | 路径规划接口降级 | 看是否触发熔断开关 | 调整超时和失败阈值 |
这个表格是我后来整理出来贴在项目文档里的,团队新成员接手时照着排查,整个定位效率高了不少。
最后再分享一点个人体会
这个项目做下来,我最大的感悟是:算法再漂亮,也要给业务留一个“反悔”和“插手”的口子。可用性筛选和距离最优原则解决的是常规场景下的效率问题,但真正让系统能被业务方接受并长期用下去的,是那套人工干预机制。它看起来只是多了一个确认页面,实际上是在算法和现实世界之间搭了一座桥。
如果你也想在自己项目里落地类似的调度分配逻辑,我的建议是先把筛选条件设计好,再考虑距离算法。筛选条件决定了下限,距离排序决定了上限。另外,双模式一定要做成可配置的,哪怕业务初期只在人工干预模式下运行,也比一上来就全自动要稳妥得多。这个内容后续还可以扩展的点是加一个分配质量回评模块,用工单完成时长和客户评分反过来校正算法权重,让系统越跑越准。