简介:滴滴打车客户端Android源码包,面向移动应用开发者及网约车业务学习者,用于剖析实际出行类App的完整实现逻辑。资源共283个文件、约5MB,其中包含36个java、121个class、39个xml、66个png、8个jar及2个apk等,java/class为源码与编译产物,xml与png对应界面布局和图标资源,jar为依赖库,apk则便于直接安装体验。整体目录覆盖乘客端、司机端、订单、支付等模块,并整理了模块化架构、多线程调度、百度地图API集成、登录注册、推送通知、支付对接及安全加密等十余类关键知识点。已有近5000人学习/下载。相比零散教程,该包能帮助读者系统理解网约车业务中定位、派单、计费到支付的完整技术链路,并通过具体代码掌握网络请求、性能优化与权限安全等工程实践,适合需要积累项目经验或从事出行类应用开发的开发者研读。 先说我看到“滴滴打车源码”这个关键词的第一反应:市面上的确流传着不少号称“滴滴源码”的打包项目,但凡真用过的都知道,下载下来能跑通的水货居多,能带你理解网约车系统核心逻辑的少之又少。而且真正深入进去会发现,打车平台这种系统,难点从来不在“能不能叫到车”,而在订单状态的一致性、派单算法的取舍、高并发下不超卖不丢单、以及司乘两端消息怎么可靠触达。这篇文章我不想再去评价某个泄露源码的真假,而是从一个后端开发者的视角,把一套打车系统从订单创建到支付完成的源码级实现思路拆开讲清楚。如果你正准备做同城出行、跑腿配送、代驾或者货运调度这类项目,这文值得收藏。
1. 打车源码不是单个项目,而是分布式系统的拆分思路
1.1 从“用户叫车”这条链路反推服务边界
很多第一次拿到类似打车源码的同学,第一反应是去MySQL里找订单表,然后顺着接口往下读。这个思路不能说错,但很容易把自己绕晕。因为一套真实可用的打车系统,源码从来不是“一个后端工程”,而是拆成了至少五六个相互独立又能协作的服务。
我自己在梳理这类项目时,习惯从一个完整的用户行为链路反推服务边界:乘客打开App、发起订单、系统派单、司机接单、司机到起点、乘客上车、行程中、到达目的地、支付账单、评价。这十来个环节里,每一步都有独立的领域模型和状态流转。你要是硬塞进一个单体应用里,前期写起来爽,等接入地图服务、实时定位、支付回调之后,代码会迅速腐化。
按我的经验,一份能称为“源码级”的打车项目,后端至少要拆成这样几个模块:
- 用户服务:乘客端和司机端的账号、登录态、乘客资料、司机资料审核。
- 订单服务:订单创建、状态流转、订单取消、订单详情查询。这是整个系统的核心。
- 派单服务:接收订单创建事件,计算候选司机,执行派单策略,推送抢单或指派。
- 司机服务:出车收车、司机实时位置维护、服务状态管理。
- 支付服务:计价、下单支付、优惠券分摊、退款、对账。
- 消息服务:面向乘客和司机的长连接推送,以及订单状态变更通知。
这里最关键的是订单服务和派单服务。很多人会觉得派单就是“找距离最近的司机”,其实真实项目里派单服务要处理的并发量和策略复杂度,远比订单服务高。后面我会单独讲。
1.2 一次真实叫车请求会经过哪些核心服务
为了让你对链路有体感,我直接画一条带时序感的请求路径。假设乘客在App首页点击“呼叫出租车”:
- 乘客端调用
order/create接口,入参是出发点经纬度、终点经纬度、车型、预计出发时间。 - 订单服务先校验用户状态、风控规则,然后创建一条状态为
WAITING的订单记录,写库成功。 - 订单创建完成后,订单服务发一条领域事件
ORDER_CREATED到消息队列(Kafka 或 RocketMQ)。 - 派单服务监听这个事件,带上订单的起终点坐标去查候选司机,执行派单算法。
- 派单服务把候选司机列表按评分和距离排序,逐个推送“新订单通知”。
- 司机端收到推送后点击“接单”,调
order/accept接口。 - 订单服务锁住订单,校验仍处于
WAITING状态,然后更新为ACCEPTED,再通知乘客端“司机已接单”。
这条链路里,订单服务只负责状态和持久化,派单服务负责实时计算,消息服务负责触达。它们之间通过消息队列解耦。这样设计的最大好处是:乘客下单那一刻,订单服务不需要关心“哪个司机能接”,派单服务也不用担心写库失败会影响下单主流程。我见过不少自己从零写打车项目的开发者,最喜欢在创建订单的接口里同步去查司机、去推送、去更新司机状态,结果单量一上来,下单接口动不动就超时。
2. 订单状态机与状态一致性:网约车源码的真正骨架
2.1 一张订单的一生:状态定义与流转规则
如果你把一个打车系统源码从头翻到尾,会发现最频繁出现的不是业务代码,而是各种if (status == xxx)的判断。订单状态机是整个系统里最不该出bug的地方,但也是很多人写得最随意的地方。
一套常规的网约车订单状态定义大概是这样的:
| 状态码 | 状态名 | 含义 | 可流转到 |
|---|---|---|---|
| 0 | CREATED | 已创建,等待派单 | WAITING、CANCELLED |
| 1 | WAITING | 待接单,正在派单中 | ACCEPTED、CANCELLED |
| 2 | ACCEPTED | 司机已接单 | TO_PICKUP、CANCELLED |
| 3 | TO_PICKUP | 司机前往乘客起点 | PICKED_UP、CANCELLED |
| 4 | PICKED_UP | 乘客上车,行程开始 | ARRIVED |
| 5 | ARRIVED | 到达目的地,行程结束 | PAYING |
| 6 | PAYING | 待支付 | PAID、CANCELLED |
| 7 | PAID | 已完成 | — |
| 8 | CANCELLED | 已取消 | — |
看到这张表,你可能会觉得这不就是枚举嘛,有什么好写的。真正的坑在于:状态只能按箭头方向走,不能跳变,更不能回退。比如一个订单在PICKED_UP状态下,乘客说司机没来,客服要取消订单,这时候代码里如果只是执行update order set status = CANCELLED where order_id = xxx,那就完蛋了——行程已经开始了,计费模块已经在跑,强制改状态会导致計费数据变孤儿。
2.2 状态流转的代码实现:状态机模式
写状态流转,我推荐用状态机模式,而不是到处散落if判断。伪代码可以这样写:
// 订单状态流转配置 Map<OrderStatus, List<OrderStatus>> TRANSITIONS = new HashMap<>() {{ put(CREATED, Arrays.asList(WAITING, CANCELLED)); put(WAITING, Arrays.asList(ACCEPTED, CANCELLED)); put(ACCEPTED, Arrays.asList(TO_PICKUP, CANCELLED)); put(TO_PICKUP, Arrays.asList(PICKED_UP, CANCELLED)); put(PICKED_UP, Arrays.asList(ARRIVED)); put(ARRIVED, Arrays.asList(PAYING)); put(PAYING, Arrays.asList(PAID, CANCELLED)); }}; public boolean transition(Order order, OrderStatus target) { List<OrderStatus> allowed = TRANSITIONS.get(order.getStatus()); if (allowed != null && allowed.contains(target)) { // 使用乐观锁更新,防止并发修改 int rows = orderMapper.compareAndSetStatus(order.getId(), order.getStatus(), target); return rows > 0; } return false; }注意compareAndSetStatus这个方法,SQL 里对应的是UPDATE orders SET status = #{target} WHERE id = #{id} AND status = #{expect}。这是状态一致性的第一道防线。因为乘客端和司机端可以同时操作同一个订单,如果没有这个条件,就可能出现:乘客点了取消,司机同时点了接单,最终状态以最后一次写入为准,完全不可控。
2.3 出车与收车:司机端状态管理
订单状态机的“上游”,还有一个很容易被新手忽略的状态:司机端的出车状态。这东西不复杂,但是很影响派单逻辑。司机出车,意味着他可以被派单;收车、服务中、暂停接单,都应该有独立的标志位。
我在实操中习惯用 Redis 保存司机实时状态和位置,而不是每次都查 MySQL:
- Key:
driver:online:{driverId} - Value:JSON 字符串,包含
latitude、longitude、status(online/offline/busy)、updatedAt。
为什么用 Redis?因为派单服务每秒钟可能要扫描上万个在线司机,如果每次扫描都去 MySQL 里查“这个司机在不在线、现在位置在哪”,数据库绝对扛不住。Redis 的 Geo 数据结构甚至可以直接做范围查询,后面讲派单算法时会再提到。
出车接口要做的事也很简单:把司机状态置为 online,写入 Redis,再异步通知派单服务“这个司机可以接单了”。收车则是反操作。但要注意,这里有竞态条件:司机已经接单了,结果他点了收车,怎么办?我的做法是,收车之前必须先检查有没有进行中的订单,有就直接拒绝,前端提示“当前有行程进行中,无法收车”。这个校验看起来简单,但漏掉的话,就会出现司机接单后失联的严重投诉。
3. 派单算法:从“最近距离”到“综合评分”的实现演进
3.1 最简单的距离优先实现
先别急着上什么智能调度,绝大多数打车源码的派单核心,本质上就是一个“在候选司机里挑一个最合适的”。最基础、也最好理解的实现,是距离优先:
- 取订单起点坐标。
- 在 Redis Geo 里查
search(起点坐标, 半径=5km, unit=m),拿到候选司机ID列表。 - 过滤掉状态不是 online 的司机。
- 按直线距离从近到远排序。
- 取前三名,依次推送抢单,直到有人接单。
伪代码大致长这样:
public List<Long> findCandidateDrivers(Position from, double radius) { Set<Long> driverIds = geoService.search(GEO_KEY, from.getLng(), from.getLat(), radius); return driverService.batchGetOnline(driverIds); }这种实现的好处是简单、快,单机 QPS 轻松上几千。坏处也很明显:它完全不管司机是不是往用户方向走、司机接单意愿高不高、司机真实距离是不是被建筑道路绕远了。所以在真实系统里,直线距离只用来圈定候选集合,真正排序要换更实际的指标。
3.2 加入等待时间、司机评分和调度约束
稍微像样一点的派单排序,用的是综合分。几个关键因子:
- 接驾距离:司机当前位置到乘客起点的预估行驶距离,权重最高。
- 司机评分:服务分高的司机优先派单,避免好单全被刷单号抢走。
- 方向匹配:司机行驶方向是否和订单方向一致,这个一般需要结合司机历史轨迹方向来判断,新手项目可以先不做。
- 派单热度:如果一个司机连续被推送了5次订单都不接,系统应降低他的优先级,甚至暂停推送几分钟。
综合分的伪代码:
public double calcDriverScore(Driver d, Order order) { double distanceScore = 1 / (1 + d.getDistanceToPickup(order.getStartPos())) * 100; double ratingScore = d.getRating() * 10; // 4.8分 => 48分 double willingScore = d.getWillingnessFactor() * 10; // 0~1 return distanceScore * 0.5 + ratingScore * 0.3 + willingScore * 0.2; }这个公式不用追求精确,核心是体现出“距离近不代表最适合”的思想。你可以按业务阶段调整权重,比如新开城想冲单量,就把距离权重调高;想保服务体验,就把评分权重调高。
3.3 推送、抢单与强制派单的取舍
派单方式有两种流派:抢单模式和指派模式。抢单是平台把订单推给多个司机,谁手快谁接;指派是平台直接指定一个司机,司机只能接受或拒绝。
从源码实现的角度,抢单模式最简单,因为司机点击“接单”时,订单状态已经从 WAITING 往 ACCEPTED 流转了,平台要做的只是保证“不能被两个司机同时抢到”。所以订单服务里compareAndSetStatus的乐观锁就是抢单的底层基础。指派模式则反过来,要由派单服务调order/assign接口,主动锁单,再发强通知给司机。
我的建议是,源码学习阶段用抢单,商业系统尽量用指派。因为抢单模式会让有经验的司机挑肥拣瘦,偏远地区单没人抢,高峰期好单被秒抢,体验很难做。指派模式配合“接单率考核”,更容易实现全局运力调度。
4. 地图与路径规划接入层的设计取舍
4.1 地理围栏、坐标转换与距离计算
打车系统离不开地图能力。我把地图这块分成四件事:坐标存储、距离计算、路径规划、轨迹纠偏。
先说坐标存储。前端传上来的经纬度,建议统一转成 GCJ-02 坐标系再落库,因为国内地图厂商用的都是火星坐标,GPS 原始坐标直接描点到地图上会偏移几十米到几百米不等。这个转换在服务端做一次就行,写到工具类里,所有入库的经纬度都过一遍。
距离计算这里有个常见的性能误区:有些开发者用 Hive SQL 或 MySQL 里直接算球面距离,量大之后慢到怀疑人生。简单场景直接用 Redis Geo 算距离就行,它是按照地球球面模型算的,误差在米级,足够用来筛选候选司机。更精确的驾驶距离,必须调地图厂商的路径规划接口,这个只能按次计费,所以要控制调用量。
4.2 路径规划结果的缓存与回退策略
路径规划接口不能每次都用真实费用去调,尤其是高频场景。我自己踩过几次教训之后,总结了一套缓存策略:
- 起点终点完全一致:直接缓存结果,key 用
route:{startLngLat}:{endLngLat},有效期10分钟。 - 方向相反:很多地图API支持返回对称路线,可以直接复用,但要记得在代码里标记方向。
- 起点或终点变化在100米内:可以认为路线不变,用哈希网格降精度匹配缓存。
还有一个必须考虑的事情:地图服务商偶尔会挂。像高德、百度这种大厂也不是100%可用,一旦路径规划接口超时,系统不能跟着崩。我的策略是设置短超时(800ms),失败后降级用直线距离 x 1.4 的粗略预估来继续派单和计价,等地图服务恢复后,再异步回填真实路线距离。
4.3 司机轨迹上报与偏航检测
司机端的轨迹不会真的每秒都往后台传,那样流量和存储都扛不住。行业里通用做法是:司机端每3秒采集一次GPS,累积5个点后批量上报一次;后台收到轨迹点之后,做抽稀处理,只保留关键转弯点,存到时序数据库或者 MongoDB。
偏航检测是另一个容易被忽略的模块。司机没按规划路线走,系统得判断是“司机抄近道”还是“司机想绕路宰客”。实现思路不复杂:每隔一段时间,拿司机最新位置和路径规划接口算出的新路线对比,如果新路线距离比原路线多了15%以上,就标记为偏航,并弹提醒。这个逻辑写起来就几十行,但非常体现项目完整度,面试时讲出来也很加分。
5. 高并发下的订单防重与支付对账
5.1 防重提交:幂等键与分布式锁
抢单场景最容易出幺蛾子的是重复提交。司机网络卡顿,点了一下没响应,又点了一下,后端如果没做防重,就会创建出两个订单——一个显示被A司机接了,另一个还在等待派单。
解决重复提交,最实在的方案是幂等键。核心逻辑是在前端生成一个全局唯一的requestId,后端在真正执行业务之前,先去 Redis 里查这个requestId是否已经处理过:
boolean first = redis.setIfAbsent("idem:" + requestId, "1", Duration.ofMinutes(5)); if (!first) { // 重复提交,直接返回上一次的处理结果 return lastResult; }这个setIfAbsent是原子操作,配合过期时间,能天然解决并发重复请求的问题。但要注意过期时间不能太短,一旦业务处理超过5分钟,锁提前过期,就会出现重复下单的漏网之鱼。
5.2 订单过期取消与异常释放
乘客下单后一直没人接,订单不能永远挂在那儿。常见策略是:下单后2分钟内若无人接单,系统自动取消,并把司机资源释放掉。
这里有人会用定时任务每分钟扫一次WAITING状态的订单,超过时间就取消。单量少没问题,单量大了就慢,而且存在“扫描到一半用户刚好叫车”的时间差。更好的方案是延时消息:订单创建时,发一条延迟2分钟的MQ消息,消息到达后再检查订单是否还在WAITING,是则自动取消,否则忽略。
要注意释放司机资源这个动作,其实在ACCEPTED的瞬间就应该完成。也就是说,司机一旦接单,派单服务要马上把他从候选池里摘掉,防止被派给下一个乘客。实现上可以在 Redis 里把司机状态置为 busy,或者从候选者集合中删除该司机ID。
5.3 支付金额计算与优惠分摊
计价和支付是打车源码里最容易跟真实业务脱节的部分。如果只是做一个静态价格 = 起步价 + 里程费 + 时长费,那太简单了,不值得写。稍微真实一点的项目,至少要包含动态调价、优惠券、免密支付、对账四个逻辑。
动态调价在高峰期把价格乘以1.2到1.8倍,实现上要注意金额精度,前端显示和小程序支付都按“分”为单位计算,后端用long类型存储金额。优惠券分摊则考验对账能力:订单实付金额 = 原始金额 - 优惠券金额 + 动态调价金额,但每一部分都要记录在订单明细表里,否则月底财务对不上账。
一个实用的小建议:支付回调必须做幂等。微信或支付宝的回调接口可能会重复通知多次(比如网络重试),如果每收到一次回调就给订单加一次“已支付”状态,金额直接翻倍。正确做法是,回调处理里先select ... for update锁住订单,检查订单状态,如果已经是PAID直接返回成功,不再执行入账逻辑。
6. 司乘端消息通道与实时通信选型
6.1 WebSocket长连接与消息推送
乘客想知道“司机到哪了”,司机想知道“乘客改起点了”,靠App轮询接口不是不行,但很费资源,而且体验差。真实项目里,司乘两端各自维护一条和后台的长连接。
技术选型上,小规模项目用 Netty 写一个 WebSocket 服务就够了,大规模再考虑引入开源IM组件。但不管用什么,核心逻辑都一样:客户端连接上来时上报自己的 userId,服务端把连接注册到本地路由表,推送的时候按 userId 找到对应的连接通道,发送消息。
这里有个坑:一台服务器只能持有它自己接受的连接。如果用户A连接的是节点1,用户B连接的是节点2,它们之间要通信,单个节点是互不可见的。解决思路是引入 Redis 发布订阅,或者用消息队列转发:节点1收到司机位置更新消息后,把消息 publish 到 Redis 的driver:location:{orderId}频道,所有订阅该频道的节点都能收到,然后节点2再把消息推给乘客端。
6.2 位置更新的批量上报与频率控制
还是那句话,位置上报不能太频繁,否则服务器扛不住。我在实战里推荐这样一个频率控制:
- 行程进行中:司机端每3秒采集,每10秒上报一次聚合点位。
- 等待接驾中:司机端每5秒采集,每15秒上报一次。
- App退到后台:停止采集,改为系统级推送通知触发定位。
类似“司机距你1.2公里”这种展示,不需要实时精确到米。后台每收到一次位置上报,都会更新 Redis 里的经纬度,同时向乘客端推送一次位置变更。如果乘客端正好也开着实时轨迹,可以每10秒从服务端拉一次最新位置平滑绘制,而不是依赖每一帧推送。
这里再提一个性能优化点:在推送轨迹点给乘客端时,不要一上来就推一串原始坐标点。先做抽稀,比如只保留间距大于50米的点,这样乘客端绘制轨迹时不会出现几百个点糊成一团的情况。
7. 从源码到实战:开源方案与复现路线建议
7.1 技术栈选型:从单机到分布式
看再多的源码,都不如自己动手把主链路写一遍。但我不建议一上来就引入微服务全家桶,而是按阶段演进:
- 单机单体版:Spring Boot + MySQL + Redis + WebSocket,把订单状态机、派单、支付回调全写在一个工程里,先实现
waiting -> accepted -> started -> finished这条主链路。 - 拆服务版:等单机版跑通,把订单服务和派单服务抽离成两个进程,引入 RocketMQ 或 Kafka 传递订单事件。这一步让你理解为什么要异步解耦。
- 加算法版:在派单服务里实现距离优先、综合评分、指派兜底三种模式,并用 Jmeter 压测对比它们的性能差异。
- 上线运维版:容器化部署,配置 Nginx 反向代理、Redis 主从、MySQL 主从,再接入监控告警。
技术栈上,Redis 的 Geo 命令是必须掌握的,它是派单的基石。搜索附近司机、计算距离、按半径过滤都是它搞定。MySQL 的表设计上,订单表的核心字段建议包含:order_id(业务单号)、passenger_id、driver_id、start_lng/lat、end_lng/lat、status、amount、create_time、update_time,并建联合索引(status, create_time)以支持“按状态查历史订单”的查询。
7.2 开发顺序建议:先搭骨架再填肉
如果你准备照着这个思路敲代码,我建议按这个顺序来:
- 先搭好多模块工程骨架,用户、订单、派单、支付、消息五个模块,内部用 Feign 调用。
- 完成用户注册登录和司机认证,先不管风控和审核。
- 完成订单表结构设计和状态机核心代码。
- 实现 Redis 在线司机管理和 Geo 查询。
- 实现抢单接口,打通“乘客下单 -> 司机接单”这条最短链路。
- 接线上的订单状态流转,司机到起点、开始行程、到达终点。
- 接入支付模拟器,先不用真实支付渠道,用“余额支付”模拟回调。
- 最后再做压力测试和细节打磨。
这个顺序的最大好处是,每一步都有可运行、可验证的成果,不会写了一个月代码还在处理数据库连接。
还有一个小技巧:项目里宁可少写花哨功能,也要把订单状态机、幂等防重、自动取消这三个模块写严谨。我在看别人简历上的“滴滴打车项目”时,最常问的就是这三个点。能把这三点讲明白的人,基本都是自己动过手的。
做这类项目,说到底是一种“用最小成本理解复杂系统”的捷径。网约车把真实世界里最棘手的实时调度、状态一致性、高并发问题浓缩到了一张订单里。你把它啃透了,去看外卖系统、快递调度、货运匹配,会发现架构思路惊人地相似。
本文还有配套的精品资源,点击获取