
做同城货运App这两年最绕不开的两个模块就是订单生命周期管理和司机匹配。最近我们项目组基于React Native做了一版鸿蒙跨平台适配司机端、用户端共用一套JS业务代码把订单创建、重量计价、车型筛选、司机指派、状态流转全部打通。这篇文章是我们从立项到上线压测的完整记录重点讲三件事基于重量分档的自动计价规则怎么落地按车型等条件筛选并排序司机的匹配逻辑怎么设计以及订单状态机在React Native和鸿蒙端之间如何保持一致。里面也包括一些文档里不写的坑比如鸿蒙原生桥接的权限适配、状态回跳的版本号控制希望能给正在做类似跨平台业务的人一些参考。1. 项目背景与整体架构设计1.1 为什么选React Native做鸿蒙跨平台我们当时同时要维护Android、iOS、鸿蒙三端业务逻辑又重又复杂。如果每个端都写一套原生代码订单计价、匹配筛选这些核心规则至少要维护三份改一个计价规则就要发三个版本测试也要回归三遍人力完全扛不住。Flutter和React Native之间也犹豫过但团队里已有的业务代码大部分是TypeScript写的前端逻辑直接复用能省很多事最后敲定React Native作为跨平台框架鸿蒙端通过社区适配的RN容器来运行同一套bundle。这里有个容易被忽视的点React Native鸿蒙适配并不是天上掉下来的它需要一个独立的运行时容器把JS逻辑映射到鸿蒙的组件和API上。选型的时候我们专门做了一个验证Demo把现有订单列表页和计价函数跑在鸿蒙模拟器上确认渲染和交互没有明显问题才正式立项。我的经验是跨平台方案不能只看谁宣传得好要拿自己项目里最复杂的三五个页面去实测重点看列表滚动性能、原生地图组件能不能用、权限申请是否流畅。如果这些核心链路都通了后续才有信心铺开。架构上我们分了三层最上面是RN业务层订单状态机、计价规则、司机筛选排序全放这里中间是桥接层负责定位、推送、相机这些原生能力的调用最底层是平台原生能力鸿蒙端跑在它的Ability框架上Android和iOS各走各的容器。这样的好处是业务代码全在JS层三端只保留一薄层原生适配后续改业务基本不用碰原生代码。1.2 订单生命周期模型拆解订单生命周期是整个业务的中枢。我们把它拆成7个核心状态待接单、已接单、司机已到、配送中、已完成、已取消、异常。每个状态都定义好“允许动作”和“目标状态”比如待接单只能被“司机接单”或者“用户取消”触发已接单只能被“司机到点”或“用户取消”触发司机到点之后才能开始配送。这样的状态机设计能直接拦掉很多业务Bug司机不可能在配送中再次接单用户取消已完成订单也会被拒绝。为了不让状态散落在各个页面里我们用一个纯TypeScript模块管理状态迁移表每个动作进来先检查当前状态是否合法。比如“开始配送”这个动作当前状态必须是“司机已到”如果服务端消息乱序导致本地还停在“已接单”这个动作会被直接丢弃同时拉取一次服务端快照纠正本地状态。用这种方式处理网络延迟和重复点击特别有效比在每个页面里写if判断要干净得多。不过状态机设计最容易踩的坑是“状态定义得太粗”或者“太细”。一开始我们只定义了待接单、配送中、已完成、取消四个状态结果运输过程中司机到底到没到点完全看不出来用户催单时客服也没法快速定位问题。后来拆细到司机已到、异常等状态才把业务链路做好闭环。建议做状态机之前先拉上运营和客服梳理一遍全流程把每个业务卡点对应到状态上宁可状态多一点也不要上线后靠异常状态来兜底。1.3 司机智能匹配的流程框架司机匹配不是一次数据库查询就能搞定的它是一条流水线。我们设计的完整流程是订单创建后先锁定候选司机池接着按车型、载重、空闲状态、距离这四个硬性条件做过滤过滤完的结果再按接单率、评分、当前任务数等因子打分排序然后向排序靠前的司机推送订单等待司机确认如果某位司机超时未确认自动顺延到下一位候选司机。为什么要分段过滤而不是一条SQL查出最终结果因为司机状态变化很快前一秒还空闲下一秒可能就被别的订单抢走了。所以我们把筛选拆成“粗筛”和“精排”两步粗筛用本地缓存的司机位置、状态数据快速过滤精排则实时向服务端要最新的接单率、评分等画像数据来计算分数。这样既保证了响应速度又让推荐结果更贴近实时状况。这个流程里最容易出问题的是司机池的锁定时机。如果订单创建后不锁定司机池等到做排序的瞬间司机已经接了其他单候选池就空了。我们的做法是在订单创建成功后立即快照一份符合硬性条件的司机ID列表后续所有推送动作都基于这份快照执行只有快照里的司机确认订单才有效这样能避免同一时间多个订单抢同一个司机的问题。2. 基于重量的自动计价逻辑2.1 计费规则与重量阶梯设计重量计价是这个业务的核心营收逻辑。我们设计的是“起步价超出重量单价”的分档计费模型不同重量区间对应不同的起步价和超出部分单价。简单说越重的货平均单价反而越低但起步价更高这样能引导用户把大件货交给合适车型也保证司机不会因为跑一趟太远而亏本。下面是我们当前线上用的默认规则重量区间(kg)起步价(元)超出部分单价(元/kg)0 ~ 5030050 ~ 200800.6200 及以上2000.5注意50kg这个边界值我们把它划到第二档也就是说50kg整的订单按第二档起步价80元来计算而不是第一档。这里有个实际教训一开始规则用的是“小于等于50”判断测试数据刚走到50kg时计价总是比预期少50元排查半天才发现是边界值判断错了。后来我们把重量区间定义为左闭右开代码里写“大于等于minWeight且小于maxWeight”才把这个问题彻底解决。计价规则不能写死在客户端。我们把规则表放在服务端配置客户端启动时拉取一次缓存到本地用于订单创建页的预估报价。这样运营调价时不需要发App版本只要推送新的规则表就能生效。但要注意规则表的版本管理同样重要否则用户看到的价格和服务端实际结算的价格可能不一致。我们在配置里加了ruleVersion字段下单时把版本号一起传到服务端服务端校验版本不一致就返回提示“价格已更新请刷新后重试”。2.2 React Native端实现自动计价计算计价函数是一个纯函数输入重量和规则表输出金额。纯函数最大的好处是容易单测也方便在React Native和鸿蒙原生端复用同一套结果。下面是我们实际在用的TypeScript实现type PricingRule { minWeight: number; // 档位下限包含 maxWeight?: number; // 档位上限不包含不传表示无上限 basePrice: number; // 起步价 extraPerKg: number; // 超出部分单价单位元/kg }; const DEFAULT_RULES: PricingRule[] [ { minWeight: 0, maxWeight: 50, basePrice: 30, extraPerKg: 0 }, { minWeight: 50, maxWeight: 200, basePrice: 80, extraPerKg: 0.6 }, { minWeight: 200, maxWeight: undefined, basePrice: 200, extraPerKg: 0.5 }, ]; export function calculatePrice(weightKg: number, rules: PricingRule[] DEFAULT_RULES): number { const rule rules.find( r weightKg r.minWeight (r.maxWeight undefined || weightKg r.maxWeight) ); if (!rule) { return 0; } const extraWeight weightKg rule.minWeight ? weightKg - rule.minWeight : 0; const extraFee extraWeight * rule.extraPerKg; const total rule.basePrice extraFee; // 规避浮点数误差保留两位小数 return Math.round(total * 100) / 100; }这段代码有两个细节值得展开。第一单位必须统一成kg。App里可能接到的是吨也可能接到的是斤但我们内部所有运算都先转成kg计价函数入口就严格要求kg一旦传错单位整个价格就错了。我们配套写了一个unitConvert函数专门处理吨、斤、公斤之间的转换并加了单元测试。第二浮点运算一定要做舍入处理。0.6乘以50之类的计算在JS里会出现类似29.999999的结果直接用生成的价格会很难看也可能导致服务端校验时不一致。我们统一用“先乘100再Math.round再除以100”的方案保证展示金额和服务端结算金额一致。下单接口提交时客户端会把重量、规则版本号、预估价格一起传给服务端。服务端拿到订单后重新按最新规则计算一遍真实价格前后端不一致时以后端为准。这样即使客户端被反编译改了函数也影响不了实际结算。2.3 边界情况处理经验计价模块的边界情况比想象中多得多。重量为负数、重量为0、重量超过最大档位上限、重量刚好卡在档位边界这些都要单独验证。我们测试组当时给出一份很长的边界用例表其中最容易翻车的是“0kg订单”用户在填写货物信息时会话中断重新进来重量没同步下单时提交了0kg。计价函数直接返回起步价30元但司机去了之后发现没货可拉纠纷马上就来了。后来我们加了前置校验下单重量必须大于0且小于等于所选车型的最大载重否则直接弹窗提示“请检查货物重量”。还有一个实战问题是App里的“预估重量”和“实际重量”不一致。很多用户随便估了一个重量下单司机到了现场才发现货物远超预估此时如果重新计价用户觉得贵了司机觉得亏了双方都要安抚。我们的折中方案是允许用户在下单后但司机接单前修改一次重量并重新计算价格一旦司机已接单重量修改必须走客服审核。这样把计价冲突尽量控制在接单前解决。经验总结计价逻辑这种核心业务宁可写起来烦一点也不能图省事。边界判断、单位转换、版本控制、服务端校验一个都不能少。我们当时把计价函数抽成独立模块写了80多个单测用例上线后因为计价问题引发的客诉明显比其他模块少。3. 按车型筛选司机与匹配策略3.1 车型数据模型和筛选条件司机端的车型信息是整个匹配系统的地基。我们用了一个简化的车型字典小型面包车、中型面包车、厢式货车、平板货车每种车型都配置了载重上限、车厢体积、车长和能否装卸货。订单创建时用户选择货物类型和重量系统自动推荐可用车型再根据这个车型要求去找司机。车型筛选属于硬性条件不满足就淘汰。过滤条件我们写成了下面这个函数这也是粗筛阶段的核心逻辑type DriverStatus IDLE | BUSY | OFFLINE | COMPLETING; function filterAvailableDrivers( drivers: Driver[], order: Order ): Driver[] { const pickupRadiusKm order.pickupRadiusKm || 15; return drivers.filter(driver { const meetsFleetType driver.vehicle.fleetType order.requiredFleetType; const meetsLoad driver.vehicle.maxLoadKg order.weightKg; const isIdle driver.status IDLE; const isWithinRange getDistanceKm(driver.lat, driver.lng, order.pickupLat, order.pickupLng) pickupRadiusKm; return meetsFleetType meetsLoad isIdle isWithinRange; }); }这里有一个实际业务摩擦点车型完全匹配往往会导致候选池过小尤其晚高峰货车数量有限。后来我们增加了“车型兼容”机制允许用户在下单时勾选“允许相近车型接单”比如厢式货车可以接平板货车的单但载重上限必须严格满足。这个逻辑也让匹配成功的司机单量提升了不少。我的建议是车型硬性条件里只有“载重上限”绝对不能放开因为它涉及行车安全其他条件都可以做成可选放宽项由用户在下单页自己决定。3.2 候选司机排序与智能推荐粗筛出来的司机可能还有几十个到底先推给谁我们用了加权评分排序。核心思想是把距离、接单率、服务评分、当前任务数这些因子做归一化然后按业务权重算出总分数。简化后的打分公式大致是这样function scoreDriver(driver: Driver, order: Order): number { const distanceScore Math.max(0, 100 - getDistanceKm(driver.lat, driver.lng, order.pickupLat, order.pickupLng) * 2); const responseScore driver.acceptRate * 20; // acceptRate 0~1 const ratingScore driver.rating * 10; // rating 0~5 const loadReduce driver.activeOrders * 5; // 当前任务数越多扣分越多 return distanceScore responseScore ratingScore - loadReduce; }这个公式看上去简单真正调参的时候才发现两个问题。第一不同因子的量纲差异很大比如距离差2公里能造成4分差距而评分差0.5分只有5分差距导致距离权重实际影响大于设计值。所以必须先做归一化距离除以最大可接单半径评分除以5接单率本身就是0到1最后再乘各自权重。第二不能只看静态画像还要看实时路况。我们第一版只按直线距离排序结果经常把司机推到拥堵路段导致到达时间延误。后来接入高德的路径规划API把预计运输时长作为一个新的排序因子匹配准确度才明显提升。排序之后还有一个推送策略的问题。一次推送太多司机会造成同一订单被很多人点到造成“手快有手慢无”的体验也很容易引发投诉。我们采用“分批推送”机制第一批推送排序前3名10秒内没有司机确认再推送第4到第6名以此类推。这样既给司机留出决策时间又避免订单在系统里挂太久。3.3 匹配超时与兜底策略匹配不是永远都能成功的。我们最常遇到的情况是凌晨用车少、极端天气司机都不出车、偏远地区候选池本来就很空。针对这些情况有三层兜底方案。第一层是超时顺延上面说的每10秒追加一批司机最多循环三次。第二层是自动扩大搜索半径初始半径15公里匹配超时后扩大到25公里同时放宽“车型兼容”选项。第三层是转入人工调度池由调度员手动在后台指派司机同时给用户推送“司机较少预计等待时间较长”的提示让用户可以选择加价呼叫。我们踩过最大的坑是“订单挂死”。当时匹配逻辑有Bug推送司机后如果司机不确认也不拒绝订单就会一直卡在PUSH_PENDING状态既不超时也不进入下一批。后来我们明确了一个原则所有推送动作必须带超时定时器超时后强制触发“无响应”事件状态机收到事件后把司机标记为未响应然后顺延下一位。同时给这个流程加了一个监控看板一旦某个订单等待司机确认超过3分钟就自动告警人工介入。4. 订单生命周期状态管理实践4.1 状态机设计从待接单到异常处理订单生命周期管理最怕状态乱跳所以我们把所有状态转移收敛到一个状态机里。下面是我们最终的简化状态表当前状态允许动作目标状态备注待接单(CREATED)司机接单已接单司机必须匹配且未被其他订单抢走待接单(CREATED)用户取消已取消取消原因必填已接单(ACCEPTED)司机到点司机已到需要上传定位快照已接单(ACCEPTED)用户取消已取消可能产生取消费司机已到(ARRIVED)开始配送配送中司机点击后记录发货时间配送中(IN_TRANSIT)确认完成已完成需要上传送达照片任意状态系统监测异常异常超时、司机失联、货物异常状态机的TypeScript实现并不复杂核心是一个迁移表加一个迁移方法。我们要求所有状态变更必须通过reducer分发action禁止在UI组件里直接改状态变量。举一个典型例子司机点击“开始配送”时会dispatch一个START_DELIVERYaction状态机校验当前状态是ARRIVED后把状态改为IN_TRANSIT同时记录时间戳。如果当前状态不是ARRIVED就直接返回旧状态并抛出一个警告日志。const transitions: RecordOrderState, PartialRecordOrderAction, OrderState { CREATED: { ACCEPT: ACCEPTED, CANCEL: CANCELLED }, ACCEPTED: { ARRIVE: ARRIVED, CANCEL: CANCELLED }, ARRIVED: { START_DELIVERY: IN_TRANSIT }, IN_TRANSIT: { COMPLETE: COMPLETED }, COMPLETED: {}, CANCELLED: {}, ABNORMAL: {}, }; export function orderReducer(state: OrderState, action: OrderAction): OrderState { const next transitions[state]?.[action]; if (!next) { console.warn([StateMachine] 非法迁移: ${state} ${action}); return state; } return next; }这段代码看起来简单但价值很大。之前订单状态乱跳导致的“幽灵单”问题基本都是因为缺少这一层统一校验。比如用户取消和司机接单同时发生时网络请求先后顺序不确定如果没有状态机本地状态可能先变成已接单又被取消事件覆盖成已取消有了状态机后取消事件在已接单状态下是允许的但接单事件在已取消状态下会被拒绝顺序就不会影响最终结果。4.2 React Native与鸿蒙端的状态同步订单状态最终权威在服务端客户端本地状态只是一份缓存。React Native业务层和鸿蒙原生层之间常常会因网络延迟产生状态不一致最常见的就是“状态回跳”用户看到订单已经是配送中又突然跳回已接单。查下来原因是服务端推送和客户端本地更新发生了乱序旧消息把新状态覆盖了。我们的解决方案是引入statusVersion版本号。服务端每发生一次状态变更就把版本号加1并随订单数据下发。客户端保存本地版本号收到新推送时先比较版本号只有新版本号大于本地版本号才允许更新状态否则把推送丢弃并触发一次服务端快照拉取。这样即使旧消息后到也无法覆盖新状态。这个方案在鸿蒙端同样适用因为比较逻辑写在RN的JS层不依赖具体端能力。还要注意React Native和鸿蒙原生组件的通信时机。鸿蒙端发起定位、扫码等原生动作时结果会通过Promise或回调传回JS层。这个桥接在状态机里属于异步事件可能出现用户连续点击多次导致多个回调并发的情况。我们做了一层“幂等处理”同一订单的同一动作在短时间内重复触发只保留一次。例如司机点击“到点”按钮如果2秒内重复点击第二次直接忽略避免状态机收到重复的ARRIVE事件后产生副作用。4.3 异常状态回滚与补偿即使有状态机保护客户端和服务端仍然可能因为网络波动出现短暂不一致。这时最重要的是“不能把错误状态展示给用户太久”。我们设计了一个补偿机制客户端在收到网络错误或者服务端校验失败时自动进入“乐观锁回滚”。比如用户在下单操作时断网本地先显示一个“下单中”的中间态页面按钮置灰重试3次仍然失败就把订单状态回滚到下单前的页面并提示“网络异常请稍后重试”。补偿机制的实现要区分“可重试”和“不可重试”的异常。可重试的包括超时、网络中断、服务端5xx错误不可重试的包括参数校验失败、请求方法不允许、状态机非法迁移。我们维护了一个错误码映射表在业务层统一捕获错误根据错误码决定是显示重试按钮还是直接跳转客服。这比每个页面各自处理错误要省力得多。另外提一个隐藏比较深的坑订单进入异常状态后司机端和用户端看到的信息可能不一样。司机端在APP里看到订单卡在“配送中”用户端却因为收到超时信号已经看到“配送异常”提示。两边都没错但对不上。我们的做法是把“异常”拆成两个层级订单内部状态还是IN_TRANSIT但增加一个displayStatus给用户看到的提示状态。内部状态驱动业务逻辑展示状态驱动UI文案两个分开后很多纠纷都清晰了。5. 鸿蒙适配与跨平台踩坑实录5.1 React Native鸿蒙适配的工程配置React Native跑在鸿蒙上最麻烦的是工程配置。我们用的方案是基于社区适配层react-native-harmony把RN容器集成到鸿蒙的主工程里。整体工程结构分两大部分一部分是标准的React Native项目负责JS bundle的构建另一部分是鸿蒙原生工程负责承载RN容器。鸿蒙工程需要在依赖文件里加入RN容器相关的库然后把模块名配置成和RN入口一致这样才能正确加载。下面是我们工程里Module配置的简化示例// 鸿蒙端入口配置示意 const reactNativeHost new ReactNativeHost() { moduleName: OrderApp, bundleAssetPath: assets/index_bundle.harmony.js, // 若用远程bundle则配置bundleUrl entryComponent: pages/Index };发布时我们把JS bundle打包成离线文件放进鸿蒙工程的assets目录。这样做的好处是启动时不依赖网络在电梯、地下车库这些信号差的地方也能正常打开App。代价是每次发版都得同时打包JS bundle和鸿蒙工程产物有一点大。我们后来做了按车型、城市拆包把基础业务bundle和地图、支付等能力动态加载启动速度才回到可接受范围。配置过程中最容易忘的还有权限声明。鸿蒙端需要在模块配置文件里声明定位、网络、相机等权限并且这些权限是运行时动态申请不能只写在配置文件里。我们当时就是漏了权限申请导致所有司机在鸿蒙手机上定位一直是0匹配池直接空了最后排查完发现是权限回调没处理。5.2 原生模块桥接与权限适配React Native自带的原生模块覆盖了常见的网络、存储、定位能力但在鸿蒙端并不是所有API都能直接用。比如我们需要读取鸿蒙系统里的车辆硬件信息、申请后台定位权限这些都需要自己封装桥接模块。我们写了一个自定义原生模块通过Promise给JS层提供接口鸿蒙端在收到请求后弹出权限弹窗用户授权后再把结果回传给JS层。// JS侧调用原生模块示例 import { NativeModules } from react-native; const LocationBridge NativeModules.OrderHarmonyLocationBridge; export async function getLocationWithAuth(): Promise{ lat: number; lng: number } { const location await LocationBridge.getCurrentPosition(); if (!location.authorized) { throw new Error(LOCATION_PERMISSION_DENIED); } return location.coords; }这里有一个经验桥接层一定要做“逻辑前置、结果纯化”。原生端只做能力调用不要塞业务逻辑返回给JS层的数据必须是纯数据对象字段名和Android、iOS保持一致。我们之前把“是否授权”这个判断写在原生端结果Android返回的字段名和鸿蒙不一样JS层判断逻辑就得写两套特别容易出Bug。后来统一成上面的结构三端行为就一致了。权限适配里还有一个冷启动问题。鸿蒙端某些机型对“后台连续定位”有省电策略司机熄屏一段时间后定位会断开导致订单位置不更新。我们在RN层做了心跳检测每隔30秒获取一次定位并上传如果连续3次拿不到定位就主动弹窗让司机重新点亮屏幕。这个交互虽然有点打扰但在货运场景里比“司机位置丢失导致订单无法指派”要可控得多。5.3 性能优化与内存泄漏排查跨平台应用最容易被吐槽的就是性能和内存。我们在鸿蒙端上线初期也遇到了两个问题页面栈越堆越高导致内存暴涨以及列表滚动卡顿。先看第一个问题。订单详情、司机列表、计价结果这些页面都是通过Navigation push进去的如果用户一直点返回上一页实际上页面实例并没有立即销毁内存会持续增长。后来我们改用Navigation replace模式在新页面打开时替换掉当前页面实例减少栈的深度。对于需要保留的草稿状态放到全局状态管理里而不是依赖页面实例。第二个问题是列表卡顿。司机列表和订单列表都是长列表我们一开始直接在ScrollView里渲染几十个卡片一到真实数据就卡。后来全部换成FlatList并给每个卡片组件加上React.memo把子组件里的临时箭头函数抽出来用useCallback包一层。这里有个小技巧不要在render里直接写内联样式对象比如style{{width: item.width}}每次渲染都会生成新对象导致子组件重新渲染。我们把这些样式抽到组件外部或用StyleSheet.create缓存列表的渲染帧率才上来。内存泄漏排查方面我们踩过一个比较深的坑在useEffect里监听原生定位事件但忘记在卸载时移除监听。因为RN容器在鸿蒙端的事件监听是全局的页面卸载后监听器还挂在原生模块上定位事件一回来JS层就报错说调用了已卸载组件的setState。后来我们统一封装了一个useNativeEventListener Hook在卸载时自动移除所有原生监听器类似的问题基本绝迹。6. 常见问题排查与优化建议6.1 问题速查表跨平台联调阶段我们遇到不少奇奇怪怪的问题这里整理一份速查表直接按现象查原因能省很多排查时间。现象可能原因处理办法订单状态从配送中跳回已接单本地状态版本号落后于服务端旧推送覆盖新状态接入statusVersion版本号小于本地则丢弃并主动拉取服务端快照重量计价多出0.00000001分JS浮点数运算精度问题金额统一用Math.round(total * 100) / 100核心计费以服务端为准重量正好50kg时价格不对边界值判断错误区间定义不一致统一左闭右开即≥min且max并写边界单测司机列表为空候选筛选条件过严或者司机定位数据为0先放宽车型兼容和搜索半径再检查定位权限与上传链路鸿蒙端定位一直为0权限未动态申请或者回调未处理检查鸿蒙配置文件权限声明以及桥接回调是否成功回传长列表滚动明显卡顿使用了ScrollView渲染大量子项组件未缓存改FlatList抽离内联样式对卡片组件包React.memo页面栈内存持续增长Navigation push过多页面实例未销毁使用replace代替push释放不需要保留的页面实例多点几次按钮导致状态异常动作重复触发但缺少幂等处理在reducer层对同一动作做节流重复事件直接忽略这张表目前放在我们的Wiki里新人接手时照着排查能少走一半弯路。其实很多问题并不是跨平台框架造成的而是业务状态设计本身没做好先把状态机和数据版本搞清晰九成的问题自然就消失了。6.2 后续优化方向目前这套系统稳定跑了两个月但我心里清楚还有不少可以优化的地方。一个是计价策略现在只按重量分档后续可以考虑加入体积、距离、楼层搬运费这些因子做一个更细粒度的动态计价引擎。另一个是匹配排序现在用的还是离线特征加简单规则后续打算把司机实时位置、历史接单偏好、时段运力热力图拼起来做一个基于评分的轻量推荐模型。这里想多说一句任何智能匹配策略都要先跑小流量实验再全量放量。我们当时改排序权重时直接在全部订单上生效结果某区域因为距离权重调低了司机跑远路的时间变长投诉率瞬间上升。后来所有排序参数都做成动态配置配合A/B实验平台灰度切换每个参数都要观察至少一周的接单率、取消率、完成时长再决定是否全量。这套流程比具体算法更重要。然后是鸿蒙端适配的体验细节后续想进一步优化冷启动动加载速度。现在离线bundle虽然稳定但首包体积还是偏大按城市、车型、用户身份做分包加载是一个明确方向。我也在观望鸿蒙原生生态的成熟度等更多原生组件可用之后可以逐步把地图、AR测量这类复杂页面从RN下沉到原生实现在性能和开发效率之间找一个更好的平衡点。最后分享一点个人体会做这种跨平台项目最大的收获不是跑通了多少功能而是学会了“把核心逻辑从界面上剥离开”。订单状态机、计价函数、司机评分这三块业务最终都抽成了平台无关的纯TypeScript模块不依赖任何React Native的API。这也意味着未来就算某一天不用RN了这些核心逻辑也能原封不动迁移到其他框架里。实际工作中这比具体技术栈选什么更重要。最后再分享一个小技巧给所有订单状态、计费规则、匹配因子都加上可观测的日志输出。我们会在状态机每次迁移时打一条日志包含订单ID、旧状态、动作、新状态、耗时。线上出问题时靠这些日志能非常快地还原订单到底经历了什么而不是靠客服截图猜来猜去。这个习惯帮我们省了无数晚睡得充实值得一试。