
1. 代驾调度系统读源码先看什么订单状态机与派单引擎我这些年看过不少出行类的系统源码也参与过几个近似场景的项目设计。说实话代驾平台的调度系统看起来跟网约车差不多但真去抠源码细节会发现它比普通网约车多了一层人在服务人的特殊约束——司机不是专职司机而是兼职的代驾师傅他可能刚在A订单结束又要赶去B订单的起点他可能今晚只接一单就回家也可能连续干到凌晨。这些业务特点全部要体现在订单状态机和派单评估里。所以读代驾调度源码我不建议一上来就扎进算法堆里而是先把订单状态机摸清楚。状态机是订单系统的骨架几乎所有风控介入点、调度触发点都挂在状态流转上。1.1 订单全生命周期的状态流转一份代驾订单从用户点击下单到司机服务结束大体会经历这样几个阶段已创建、待匹配、已指派、司机已接单、司机已到达、服务中、服务完成、已支付、已取消。源码里常见的实现方式是枚举加状态流转表而不是在各个业务方法里随便改状态。用伪代码大概是这样public enum OrderStatus { CREATED, MATCHING, ASSIGNED, DRIVER_ACCEPTED, DRIVER_ARRIVED, IN_SERVICE, FINISHED, PAID, CANCELLED } // 允许的流转路径 MapOrderStatus, SetOrderStatus allowedTransitions Map.of( CREATED, Set.of(MATCHING, CANCELLED), MATCHING, Set.of(ASSIGNED, CREATED, CANCELLED), ASSIGNED, Set.of(DRIVER_ACCEPTED, MATCHING, CANCELLED), DRIVER_ACCEPTED, Set.of(DRIVER_ARRIVED, CANCELLED), DRIVER_ARRIVED, Set.of(IN_SERVICE, CANCELLED), IN_SERVICE, Set.of(FINISHED), FINISHED, Set.of(PAID), PAID, Set.of() ); boolean canTransit(OrderStatus from, OrderStatus to) { return allowedTransitions.getOrDefault(from, Set.of()).contains(to); }为什么一定要用这种白名单式的状态流转因为订单状态是钱和责任的凭证。已支付订单如果被错误地回退到服务中会直接导致结算和风控账单错乱。我见过一个真实教训某个系统为了方便测试人员改状态给后台接口留了万能更新入口结果线上一个误操作把一批已完成订单状态打回服务中司机的钱包账户和用户账单对不上最后只能靠人工对账补救。所以正规系统的源码里状态变更几乎都是统一走一个changeState方法里面先校验canTransit再写变更流水最后发事件。代驾场景还有一个容易被忽略的状态司机已到达后的等待阶段。用户可能因为醉酒在楼上磨蹭司机在路边等待这时候订单仍然占着司机的接单资格系统需要有个等待中的子状态并且要开始计费等逻辑。这个子状态如果在源码里没有单独建模而是放在DRIVER_ARRIVED里硬堆字段后期加等待超时自动取消等待费结算用户位置再确认这些功能都会非常痛苦。1.2 派单引擎的常见实现逻辑状态机之上最核心的就是派单引擎。代驾平台的派单策略跟网约车不完全一样。网约车大多是最近的空车接最合适的单但代驾要考虑更多司机骑折叠电动车或坐地铁到用户起点他的到达时间和接单意愿跟距离不完全成正比司机服务分高的可能更会处理醉酒用户还有车型匹配——用户叫了豪华车代驾普通电车师傅接不了。我在源码里常见的实现是一个多层筛选加评分的流程圈选候选司机从Redis地理索引里拉取订单起点周边3到5公里内、在线且未被锁定的司机。硬性过滤剔除资质过期、被风控临时限制接单、连续服务时长超限、酒测未过的司机。特征评分对每个候选司机算一个综合分。生成指派请求按分数排序向Top N司机发送指派通知等待接受。评分伪代码大概是这样的def score_driver(driver, order, config): distance_score max(0, 1 - haversine(order.start, driver.pos) / config.max_distance) service_score normalize(driver.service_score, 0, 5) accept_rate_score driver.accept_rate / 100 model_score config.model_match.get(order.car_model, 0.2) return 0.4 * distance_score 0.3 * service_score 0.2 * accept_rate_score 0.1 * model_score权重系数一般不在代码里写死而是配置中心下发。我读这类源码时有个经验第一遍先看默认权重值第二遍再看配置中心的默认配置两者经常不一样而真正线上跑的是后者。所以光读代码不读配置很容易误判派单实际逻辑。代驾派单还有一个细节它是先指派后确认还是直接抢单。国内平台更多用指派模式因为代驾订单密度不像网约车那么均匀抢单模式容易造成大量空跑。源码层面指派模式通常需要处理一个棘手的并发问题同一个司机同时收到两个指派请求以及同一个订单被两个调度实例同时处理。典型解法是用Redis分布式锁加一个dispatchRequestId做幂等比如-- 伪代码锁定司机 local ok redis.call(SET, driver_lock: .. driverId, orderId, NX, EX, 5) if ok then -- 生成唯一的派单请求ID消息队列里去重 end这个锁千万不能只在代码里写一遍就放心因为锁的超时时间、续期机制、异常释放任何一个环节出问题都可能导致司机明明空闲却一直收不到单或者更糟——同一个司机同时接了两单。线上问题往往是锁没释放导致的司机饿死而不是锁没拿到。1.3 位置、轨迹与调度数据的组织方式调度系统离不开司机位置。源码里面位置数据一般分两层一层是高频心跳位置秒级或分钟级写入Redis用于圈选司机另一层是订单轨迹服务中每几秒记录一个轨迹点最终落到时序数据库或HDFS用于事后回放和风控分析。这里有个安全合规直接相关的点调试或日志打印时绝对不能把完整经纬度一把梭地打出来。我见过某项目把司机实时位置打进了业务日志本意是排查派单异常结果日志被无关人员拿到相当于把全城司机的行动轨迹泄露了。后来整改成位置脱敏打印比如只输出39.90xx, 116.39xx这种模糊到几百米的粒度排查够用风险小很多。2. 风控不是独立服务而是嵌在调度链路上的约束器很多研发一听到风控就想象成一个独立的系统接收请求、返回风险分、完事。真实代驾平台的风控完全不是这个形态。风控模块出生就嵌在调度链路上它不是在旁边看的而是掐着订单流动的每一个咽喉要道。这也是我读风控源码时体会最深的一点。2.1 代驾特有的风控场景人证合一与酒精检测代驾的风控对象主要是司机端而且有几个行业特殊场景普通网约车源码里根本见不到。第一是人证合一。代驾司机账号必须和实际驾驶者一致。源码里一般设计为开工前核验流程司机打开App点击上线触发人脸采集拿到照片后与注册证件照做人脸比对比对通过才允许听单。这中间还涉及活体检测——防止拿照片翻拍。源码上通常是一个异步流程人脸服务商回调结果风控侧更新司机状态再通知调度侧开放接单权限。第二是酒精检测。这是代驾和所有出行平台最大的不同。司机上线前或接单前必须做酒精吹气检测读数超过阈值直接限制接单。我在阅读相关源码时注意到酒精检测数据不能只存在App本地否则司机端被绕过就形同虚设。规范做法是检测设备或手机传感器采集的数据要加密上报服务端留存检测时间、设备编号、结果值并且这个数据要参与调度门禁判断。# 风控门禁伪代码调度前检查 def can_assign(driver): if not driver.face_verified_today: return False, FACE_NOT_VERIFIED if driver.alcohol_value and driver.alcohol_value 0.3: return False, ALCOHOL_LIMIT_EXCEEDED if driver.continuous_online_seconds 4 * 3600: return False, FATIGUE_LIMIT_EXCEEDED return True, OK这些规则在源码里通常被做成了规则配置而不是硬编码。真正的代码主体是一个规则引擎加载规则、匹配事件、执行动作。如果你只读can_assign这个函数会以为风控逻辑就十几行其实十几行背后挂着一堆从配置中心来的动态规则。2.2 规则引擎与实时特征计算的源码实现思路代驾风控的规则引擎业界常见有三种实现路线第一种是纯代码规则。简单直接但改规则要发版适应不了快速变化的黑产手段现在很少作为主力。第二种是配置化规则。规则存在DB或配置中心用JSON、YAML或者类SQL的语言描述。线上运营人员可以通过后台调整阈值但需要设计好版本管理否则改错一个参数就是全网性事故。第三种是基于Drools、Flink CEP这类规则引擎或复杂事件处理框架。适合做时序类的风控比如司机在5分钟内连续取消3单这种窗口计算。一个典型的配置化规则长这样rule_risk_events [ { event: DRIVER_ACCEPT_RATE_ABNORMAL, condition: accepted_count / (accepted_count rejected_count) 0.3, window: last_1_hour, action: TEMP_BLOCK_ASSIGN, duration: 600 }, { event: PATH_DEVIATION, condition: current_route_offset 500m, window: last_3_minutes, action: TRIGGER_SOS_REVIEW } ]规则引擎的核心模块是事件输入、规则解析、条件匹配、动作执行、审计留痕。源码阅读顺序建议先看动作执行——也就是风控命中后到底干了什么这决定了它对调度链路的侵入程度。有的动作是同步拦截直接阻断指派有的是异步打标后续订单降权有的是转人工审核。搞清楚这些之后你才能理解为什么有些风控没有立刻生效。特征计算这块代驾场景和电商风控差异也很大。电商风控看的是设备指纹、IP、支付频次代驾风控大量依赖时空特征司机当前位置、速度、轨迹弯曲度、停留时长、订单起点周围环境的POI类型。比如一个代驾司机正常应该在路边或停车场等待结果定位一直停在某个小区楼栋内超过20分钟这可能是司机在睡觉也可能是账号被异地登录。这类特征计算往往需要实时流处理框架配合Flink是常见选择因为要在秒级窗口内完成地理围栏计算和轨迹特征提取。2.3 风控决策结果如何回写调度流程风控决策结果回写调度是源码里耦合度最高的一环。我会特别关注回写链路是同步还是异步。同步回写适合必须实时拦截的场景比如黑名单司机试图接单调度前检查直接返回失败整个流程在几百毫秒内完成。异步回写适合画像类和事后分析类比如司机服务分下降、被标记为醉酒用户投诉高发司机这些不需要立刻拦截但会影响下一次派单的评分。源码里一般会用消息队列做异步解耦。风控命中后发一个RiskHitEvent调度消费者更新司机状态或订单标签。这里最常见的问题是消息丢失和重复消费。如果消息丢了司机明明被风控限单调度侧却不知道继续派单等出事了才在审计里发现风控消息根本没送达。所以我在阅读这类源码时会重点看消费端是否有幂等表、是否有对账任务。3. 国内外平台在安全合规上的代码层面差异标题里提到国内外我特意比较过国内外代驾平台在这方面的设计取向发现差异非常明显。我不谈具体国家和具体法律条文只谈技术实现层面大家普遍采纳的方案差异。3.1 数据字段设计与脱敏策略国内平台的数据设计倾向于先采集、后授权、再管控。也就是说很多字段在业务需要时先采集包括乘客手机号、精确位置、行程录音、人脸照片等后续再通过脱敏、加密、权限管理来控制风险。国外平台更倾向于先声明用途、最小化采集、再处理。它们的代码里常常可以看到字段级别的访问控制注解或隐私偏好对象。不管哪种取向代码层面有几个通用做法是必须的手机号脱敏。司机和乘客的真实手机号不能全程明文传递。服务端通过隐藏号或中间号平台建立通话App界面上只显示脱敏后的号码。脱敏工具类一般是通用组件public class MaskUtil { public static String maskMobile(String mobile) { if (mobile null || mobile.length() 11) return ; return mobile.substring(0, 3) **** mobile.substring(mobile.length() - 4); } }位置精度分级。调度算法需要高精度坐标但日志、结算、客服展示等场景只需要低精度。源码里通常存在一个坐标精度处理层输出到不同场景前做不同粒度的偏移或模糊化。证件号与生物特征。这类强敏感数据必须加密存储而且不能仅仅AES一把密钥到底。规范一点的做法是字段级加密加密钥分层数据读取走专门的解密网关所有解密操作都要审计。我见过的小型代驾项目经常把身份证号直接明文放在数据库里连个脱敏视图都没做这类问题在源码评审阶段就该被拦下。3.2 日志审计与合规链路追踪安全合规视角下日志不只是用来排障的更是用来证明系统没有违规操作的依据。调度和风控涉及的敏感操作很多比如客服改单、风控人员解封司机、后台查看用户位置、撤销订单等。这些操作必须被审计。源码上审计日志通常用AOP或注解方式实现AuditLog(action UNLOCK_DRIVER, scene RISK_CONTROL, needReason true) public void unlockDriver(Long driverId, String operatorId, String reason) { // 业务逻辑 }这个注解背后会记录操作人、操作时间、操作对象、请求参数摘要、操作理由、结果状态。真正严格的系统审计日志是只追加且不可篡改的常常使用专门的存储来保证防删除。链路追踪在合规里的作用是回答一次风控拦截的完整链路是什么用户下单、调度请求、风控判定、拦截结果、客服申诉、人工复审。源码里要紧盯事件ID或请求ID是否贯穿全链路。我见过的问题是调度系统和风控系统各记各的日志ID字段命名都不一样出事后想串起来得靠时间戳和经纬度猜极其痛苦。后来统一在入口生成traceId所有日志输出带上它才好很多。3.3 用户授权与隐私偏好设置的实现隐私偏好是国内外代驾系统代码差异的典型区。国外版本的App通常有专门的面板让用户自行决定是否共享精确位置、是否允许行程录音、是否接收个性化推荐。对应的服务端代码里业务读取敏感字段前会检查用户偏好def get_user_location(user_id): preference privacy_pref_repo.get(user_id) if not preference.can_share_location: return anonymize(coarse_location_cache.get(user_id)) return precise_location_repo.get(user_id)国内很多系统的做法是默认采集、默认录音用户只能一键关闭全部授权颗粒度没那么细。但从安全合规的技术视角看两种做法都需要在代码里实现三个基本能力记录用户授权版本用户看到过哪个版本的隐私政策同意过什么、提供撤回授权的接口、撤回后数据如何处理清理还是匿名化。这块做得好不好读源码时一眼就能看出来。好的系统授权记录是一个实体表里面包含user_id, agreement_version, authorized_at, revoked_at而不是一个简单的布尔字段。布尔字段的问题是你把同意录音从true改成false系统怎么知道用户当初同意的是哪个版本的条款如果条款变更是不是所有老用户都要重新同意这些在合规审计中是会被问到的。4. 高并发下的调度与风控联调事件总线、超时降级与最终一致调度系统和风控系统节奏天然不一样。调度追求快一单来了最好毫秒级分出去风控追求准要尽可能多算一点特征再下结论。这两个系统在源码层面怎么捏合在一起是最见功底的地方。4.1 事件驱动架构把调度和风控解耦现代代驾平台几乎不可能用一个大单体同时承担调度和风控。常见做法是事件驱动订单状态变化、司机位置变化、酒测结果、支付结果全部作为事件发到消息总线。风控服务订阅事件算出风险结论后再以事件或回调方式影响调度。好处是解耦坏处是要处理分布式系统最难的那批问题乱序、重复、丢失。我读源码时会特别留意事件消息的key设计。比如订单事件用orderId做key保证同一个订单的事件顺序司机位置事件用driverId做key但要允许不同司机并发处理。如果key设计错了同一个司机的最新位置可能被旧位置的迟到消息覆盖调度拿到错误的位置去派单后果是司机明明到了系统还显示在3公里外。4.2 超时与降级策略不能让风控拖死调度代驾是强时效场景用户在雨夜路边等代驾系统不能让他在原地等5秒钟做风控判断。所以源码里的一个关键设计是超时控制。主链路上调度前检查类的风控通常有硬超时比如300毫秒。超过这个时间宁可放行也不能让用户干等。但放行不等于不处理而是先记录未决风险后台异步补查如果补查发现高风险再紧急通知用户或客服介入。这相当于把风控分成了快速闸门和慢速布防两道线。快速闸门适合用的规则很简单黑名单匹配、账号状态检查、接单资格检查这几类查Redis就够毫秒级返回。慢速布防适合复杂模型疲劳度模型、路径偏差分析、人群画像这些需要窗口计算和算法推理不适合阻塞实时派单。我在实际项目里见过一个惨痛案例把一套复杂的图计算模型塞进派单同步链路结果模型服务一抖动整个派单接口平均耗时从200毫秒涨到3秒线上订单成交量直接跳水。后来改造成异步风控打分调度的主链路只保留Redis黑名单快速检查和本地缓存的门禁规则复杂判断全部放异步响应时间才恢复。def assign_driver(order): # 主链路只做快速风控 quick_risks check_quick_risks(order.driver_candidates) # 如果快速风控超时默认放行并记录未决事件 if quick_risks.timed_out: risk_event_bus.publish(PendingRiskEvent(order.order_id)) quick_risks PassResult() # 派单评分主流程 candidates filter_by_quick_risk(quick_risks) target select_best_candidate(candidates) # 异步补充慢速风控 risk_event_bus.publish(AsyncDeepCheckEvent(order.order_id, target.driver_id)) return target4.3 最终一致性订单状态与风控标签的对账一旦引入异步操作就不可能保证每个瞬间都强一致。比如风控异步判定某司机为高风险要求立刻限制接单但司机此刻已经在服务中订单里系统怎么办总不能把车停在路边把人赶下去。所以源码里做的往往是软限制当前订单继续服务但服务结束后司机状态变为冻结同时不再向该司机派新单。订单状态和风控标签的最终一致依赖对账任务来实现。每天凌晨跑一个批处理扫一遍风控标记了限制接单但司机状态仍是可派单的数据调用状态纠正逻辑。这个对账任务的SQL怎么写读源码时值得好好看。好的实现会把对账做成可重入的跑挂了能重跑不会重复扣钱或重复发惩罚。另一个容易出问题的点是补偿顺序。风控解封司机时必须先更新风控中心状态再通知调度放开接单反过来封禁司机时要先在调度侧停止派单再在风控中心落标记。顺序反了就可能出现调度已经派了一单风控中心却还没标记上司机侥幸接了一单。很多线上安全事故就是这么来的。5. 源码阅读的实操路线哪些模块值得精读最后聊点实际的。如果你不是代驾平台的员工而是想通过阅读源码提升自己应该怎么读市面上不太可能有哪家代驾公司把生产级完整源码公开我们更多是读三类东西开源出行项目、Spring StateMachine、Drools/Flink CEP这类组件的源码以及从技术博客、架构分享、招聘JD里反推业务源码结构。5.1 从开源项目反推商业代驾系统的设计我推荐先读Spring StateMachine和Drools的官方示例因为它们几乎是状态机和规则引擎的事实标准。把这两个玩熟再看开源的网约车或跑腿配送项目重点关注以下几个模块的位置order或dispatch目录下有没有独立的状态机包还是状态散落在service层。risk或security目录有没有规则配置的DB初始化脚本看初始规则能猜出平台最在意哪些风险。有没有统一的audit或log模块敏感操作是否走AOP统一切面。配置中心或config目录里的默认值往往比代码更有信息量。用这种方式读三四个开源项目你会形成一种源码嗅觉拿到一个陌生系统能快速定位调度主链路、风控门禁、审计模块在哪里。真正工作里接手一个老系统这种嗅觉比背多少面试题都管用。5.2 值得精读的环节派单评分、规则脚本引擎、人脸回调派单评分网上能找到很多简化版实现但我建议精读的时候不要只看公式要重点看两个附加模块一是评分日志即每个候选司机最终得分是怎么算出来的要能追溯二是人工干预接口比如客服把某个司机从某订单中剔除这类操作有没有留痕、有没有同步给派单流。没有这两块的派单评分实战里根本不敢上线。规则脚本引擎值得精读的是Groovy或Spel脚本怎么加载、怎么沙箱化。风控规则经常要让风控运营自己改但绝不能让他们写一段System.exit(0)就把服务搞挂。好的实现会有脚本白名单、超时熔断、资源限制。人脸回调模块值得精读的是回调接口的幂等设计。人脸识别服务商回调两次系统不能给司机发两次上线通知也不能在回调延迟10秒时让司机一直干等。这类回调接口的源码里往往藏着最接地气的工程智慧。5.3 我读这类源码时踩过的坑踩坑一只读业务代码不读配置中心下发的东西。风控规则真正的阈值、派单权重的真实系数百分之九十不在代码里而在配置中心。光看代码会觉得系统很简单实际上运行中的系统是一套代码加动态配置的复合体。踩坑二忽略异步回调和消息补偿。看源码时如果你只盯着同步调用会以为某条风控规则没生效其实是生效了但走了异步路径结果晚了几分钟才落到订单上。线上排查时这类误判特别浪费时间。踩坑三把开源项目的单机实现当成生产实现。开源项目很多是单机内存队列搞定的事生产环境是Kafka加多副本加幂等表。完全不是一个量级读的时候要分清哪些是演示性简化哪些是真正可落地的设计。我个人读这类带调度和风控的系统源码最受益的顺序始终是先画状态机再追一笔订单从创建到完成的整个事件流最后才看具体算法和规则。状态机给你骨架事件流给你脉络算法和规则是血肉。跳过前两步直接扑到算法上基本等于看树不见林。代驾平台也好其他出行平台也好调度和风控从来不是两个独立课题——调度决定效率上限风控决定安全下限合规则是画在系统架构图上不可见却无处不在的边界。能把这三者的耦合关系在源码层面看明白读这类系统的功力就算真正到位了。