简介:飞滴网约车项目-online-taxi-public.zip 是一套面向网约车业务场景的开源学习工程,适合具备一定 Java 与分布式基础、希望深入理解在线打车平台架构的开发者。项目围绕乘客叫车、司机接单、订单流转、计费结算等核心链路展开,涵盖用户注册登录、位置定位与地图集成、车辆与司机管理、订单匹配与状态更新、支付结算、异常处理及安全合规等模块,可作为构建高并发实时系统的参考范例。压缩包共163个文件,以132个 Java 源码为主体,辅以19个 XML 配置、9个 YML 配置文件及少量说明文档,整体约137KB,目录结构清晰,便于按业务模块检索阅读。目前已有687人学习下载。通过研读源码,读者可掌握订单状态机设计、司机与乘客匹配算法、计费规则实现以及服务间调用关系,并借鉴其数据库模型与接口定义方式,为网约车类项目开发或分布式系统学习提供可落地的实践素材。
1. 从一份网约车源码包说起:它到底能跑通什么
如果你正在找一个能完整跑通「乘客下单 → 司机接单 → 行程计费 → 订单结算」全链路的练手项目,online-taxi-public.zip大概率会出现在你的候选清单里。它是一套网约车业务的后端源码包,覆盖了乘客端、司机端、订单调度、计价规则、支付回调这几块核心域,技术栈以 Java 服务端为主,配套数据库脚本和接口文档。适合两类人:一是想拿一个真实业务密度足够高的项目练手、把分布式和状态机落地一遍的初中级后端;二是需要一套可裁剪骨架、快速搭出 demo 给业务方看的技术负责人。它不解决算法层面的路径规划,也不含前端 UI,重点全在业务编排和数据一致性上。
我拆这份包的时候,第一反应是「业务域切得比想象中细」。订单状态、司机状态、计价快照、支付流水是分开建模的,这意味着你没法用一张大表糊过去,必须认真处理状态流转和并发。对想练手的人来说这是好事,对想直接上线的人来说,你得先想清楚哪些模块要留、哪些要砍。下面按「先看懂结构 → 再跑起来 → 再改参数 → 最后避坑」的顺序拆,每一步都落到能抄的代码和能改的参数上。
2. 拆包先看骨架:模块划分与订单状态机怎么读
拿到压缩包别急着mvn spring-boot:run,先花二十分钟把目录和状态机读明白,后面能省掉大量「为什么订单卡住不动」的排查时间。这套源码的模块划分基本遵循「按业务域分包 + 按层分层」的混合方式,理解了这个约定,你找任何一段逻辑都不会迷路。
2.1 目录结构与模块职责
解压后典型结构大致是这样(不同版本目录名可能略有差异,以实际为准):
online-taxi-public/ ├── api-common/ 公共依赖:统一返回体、常量、工具类 ├── service-order/ 订单域:下单、状态流转、取消 ├── service-driver/ 司机域:司机状态、接单、听单 ├── service-passenger/ 乘客域:乘客信息、行程查询 ├── service-price/ 计价域:计价规则、费用快照 ├── service-pay/ 支付域:支付回调、流水 ├── sql/ 建表脚本与初始化数据 └── docs/ 接口说明读的顺序建议是:先看api-common里的统一返回体和状态常量,再看service-order的状态流转,最后看service-price和service-pay怎么跟订单挂钩。原因是订单是主线索,其他域都是围绕订单状态变化触发的。很多人一上来就钻计价算法,结果发现计价依赖订单里的「行程开始时间」和「里程快照」,没有订单上下文根本看不懂。
提示:如果
sql/目录里的建表脚本和实体类字段对不上,以实体类为准,脚本可能是早期版本。这是这类源码包最常见的「文档滞后」问题。
2.2 订单状态机:整条链路的骨架
网约车业务的核心就是订单状态机。这套源码里订单状态大致经历这几个阶段:
| 状态值 | 含义 | 触发动作 |
|---|---|---|
| 1 | 新建待派单 | 乘客下单成功 |
| 2 | 已派单待接 | 系统派单给司机 |
| 3 | 司机已接单 | 司机确认接单 |
| 4 | 行程中 | 司机到达上车点并开始行程 |
| 5 | 行程结束待支付 | 到达目的地 |
| 6 | 已支付完成 | 支付回调成功 |
| 7 | 已取消 | 乘客或司机取消 |
状态流转必须单向,任何「回退」都要走取消再重建。我见过有人为了图省事,在支付失败时把状态从 5 改回 4,结果计价快照和实际行程对不上,对账时全是坑。正确做法是支付失败保持状态 5,另起一个支付重试记录,订单状态不动。
读状态机的时候重点看两处:一是每次状态变更有没有写「状态变更日志表」,二是变更时有没有做乐观锁或版本号校验。前者决定你能不能排查问题,后者决定并发下会不会出现「两个司机同时接到同一单」。这套源码在接单环节用了数据库行锁 + 状态条件更新,是常见做法,但你要确认自己的数据库隔离级别和它匹配。
-- 接单时的条件更新,防止一单多接 UPDATE order_info SET status = 3, driver_id = #{driverId}, accept_time = NOW() WHERE order_id = #{orderId} AND status = 2; -- 只有待接单状态才能被接 -- 影响行数为 0 说明被别人抢先接了,业务层要处理这个分支这段 SQL 的逻辑是「带状态条件的原子更新」,参数status = 2是前置条件,driver_id和accept_time是本次变更写入的值。执行后必须检查影响行数,为 0 就返回「订单已被接走」,不能当成成功。很多人漏掉这个判断,压测时就会出现同一订单绑定两个司机。
3. 把项目跑起来:环境、建表与接口自测
骨架读懂了,接下来是让它真正跑起来。这一步的坑主要集中在环境版本、数据库初始化和服务间调用三块。我一般会先在一个干净环境里跑通单服务,再逐个把依赖服务拉起来,避免一上来全启动、报错信息糊成一团。
3.1 环境准备与依赖版本
这套源码是 Java 服务端项目,常见依赖是 JDK 8 或 11、Maven 3.6+、MySQL 5.7/8.0、Redis。版本对不上是最常见的翻车点,尤其是 JDK 版本和 Spring Boot 版本不匹配时,启动直接报类找不到。
# 确认基础环境,版本对不上先别往下走 java -version # 建议 JDK 8 或 11,看 pom 里的 source 配置 mvn -v # 3.6 以上 mysql --version # 5.7 或 8.0 redis-cli ping # 返回 PONG 才算通参数说明:java -version看的是运行时版本,但真正决定编译行为的是pom.xml里的maven.compiler.source。如果 pom 写的是 1.8 而你本地是 17,编译能过但运行可能出问题,稳妥做法是本地装一个和 pom 一致的 JDK。Redis 主要用在司机听单的在线状态和订单缓存,没起来的话司机端接口会超时,但订单主流程还能走,别被这个误导。
3.2 建表与初始化数据
数据库这块,先执行sql/下的建表脚本,再按需导入初始化数据。注意脚本里的库名要和配置文件里一致,否则启动时报「表不存在」,你会以为是实体映射问题,其实是连错库了。
# 建库建表,库名以配置文件为准 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS online_taxi DEFAULT CHARSET utf8mb4;" mysql -uroot -p online_taxi < sql/schema.sql # 初始化基础数据:计价规则、城市配置等 mysql -uroot -p online_taxi < sql/init_data.sql逻辑说明:schema.sql负责表结构,init_data.sql负责计价规则这类「没有就跑不通」的基础数据。参数上重点是字符集用utf8mb4,网约车业务里地址、备注可能含特殊字符,用utf8会插入失败。执行完用SHOW TABLES;确认表数量,再抽查price_rule表有没有数据,空的话计价接口会返回 0 元,别以为是算法坏了。
3.3 启动服务与接口自测
单服务启动用 Maven 就行,关键是配置文件里的数据库、Redis 地址要改成本地的。启动顺序建议:先service-order,再service-driver和service-passenger,最后service-price和service-pay。
# 在 service-order 目录下启动,其他服务同理 mvn spring-boot:run -Dspring-boot.run.profiles=dev启动后别急着调业务接口,先用一个最简单的下单接口验证链路通不通:
# 模拟乘客下单,参数按实际接口文档调整 curl -X POST http://localhost:8081/order/create \ -H "Content-Type: application/json" \ -d '{ "passengerId": 1001, "startAddress": "模拟起点A", "endAddress": "模拟终点B", "cityCode": "1001" }'参数说明:passengerId要对应乘客表里真实存在的记录,cityCode要能在计价规则表里查到,否则下单会失败在计价初始化那一步。返回体里重点看orderId和status,status应该是 1(待派单)。如果返回的是统一错误体,先看错误码,再顺着日志找,别盲猜。这一步通了,说明订单域、计价域、数据库三方是打通的,后面接单和支付才有意义。
4. 计价与支付:参数怎么设、回调怎么验
订单能创建只是开始,真正体现业务复杂度的是计价和支付。这两块也是最容易「看起来跑通了、一算账全错」的地方。计价的核心是「快照」,支付的核心是「幂等」,把这两个词吃透,这块就稳了。
4.1 计价规则与费用快照
计价不能实时算,必须在行程结束时把当时的规则「拍个快照」存进订单,否则规则一改,历史订单金额全变。这套源码里计价域提供规则查询,订单域负责在行程结束时调用并落快照。
// 行程结束时生成费用快照,伪代码示意 PriceRule rule = priceService.getRule(cityCode, carType); BigDecimal distanceFee = rule.getPerKmPrice() .multiply(new BigDecimal(trip.getDistanceKm())); BigDecimal timeFee = rule.getPerMinPrice() .multiply(new BigDecimal(trip.getDurationMin())); BigDecimal total = rule.getStartPrice() .add(distanceFee).add(timeFee); // 关键:把规则版本和明细一起存进订单,而不是只存总额 order.setPriceSnapshot(JSON.toJSONString(rule)); order.setTotalAmount(total);逻辑说明:getRule按城市和车型取规则,distanceFee和timeFee分别按里程和时长算,最后加上起步价。参数上最容易出错的是「里程和时长的单位」,规则表里可能是「元/公里」,而行程表里存的是「米」,不换算就会算出天价或零元。所以我在快照里连规则版本一起存,出问题能回溯当时用的是哪版规则。这一步的坑在于「只存总额不存明细」,对账时你根本说不清钱是怎么来的。
4.2 支付回调与幂等处理
支付回调是外部系统打进来的,网络重试、重复通知是常态,所以回调处理必须幂等。这套源码里支付域收到回调后,先查流水表有没有这条记录,有就直接返回成功,没有才更新订单状态。
// 支付回调幂等处理 PayFlow exist = payFlowMapper.selectByOutTradeNo(outTradeNo); if (exist != null && "SUCCESS".equals(exist.getStatus())) { return "success"; // 已处理过,直接应答,避免重复改单 } // 首次处理:写流水 + 改订单状态,放在同一事务里 payFlowMapper.insert(buildFlow(outTradeNo, amount)); orderService.markPaid(orderId, outTradeNo); return "success";参数说明:outTradeNo是外部支付单号,必须建唯一索引,这是幂等的最后一道防线。markPaid里要再次校验订单状态是不是 5(待支付),不是就忽略,防止「已取消的订单被支付成功」。常见翻车是回调里只改状态不写流水,结果重复回调时状态被改两次,或者对账时找不到支付凭证。把流水和状态变更放同一事务,是这类场景的稳妥做法。
注意:回调接口的应答内容要严格按对接方要求返回,返回错了对方会一直重试,日志会被刷爆,排查时容易被误导成「回调失败」。
5. 避坑与排查:这几处最容易翻车
这套源码我前后拆过几遍,下面这几条是复现时出现频率最高的,每条都按「现象 → 原因 → 解决」写清楚,照着排查能省不少时间。
现象一:下单成功但一直没人能接单。原因通常是司机听单的在线状态没写进 Redis,或者派单逻辑依赖的「司机状态」字段初始值不对。解决是先确认司机端有没有调用「上线」接口,再查 Redis 里有没有对应的在线 key,最后看派单查询的条件是不是把新司机过滤掉了。
现象二:订单状态卡在「行程中」不动。原因多是行程结束的触发依赖客户端上报,而上报接口因为参数校验失败被拒了。解决是看行程结束接口的入参校验日志,重点核对里程、时长字段的类型和单位,很多时候是前端传了字符串、后端要数字。
现象三:计价金额为 0 或异常大。原因是计价规则没初始化,或者里程单位没换算。解决是先查price_rule表有没有对应城市和车型的数据,再核对行程表里里程字段的单位和规则表是否一致,不一致就在快照生成前统一换算。
现象四:支付回调重复触发导致订单状态错乱。原因是回调没做幂等,或者幂等判断和状态更新不在同一事务。解决是给支付单号加唯一索引,回调里先查流水再处理,流水写入和订单状态更新放同一事务。
现象五:服务启动报「表不存在」但表明明建了。原因是配置文件连的库和建表脚本执行的库不是同一个,或者大小写敏感配置不一致。解决是核对application.yml里的库名,以及在 MySQL 配置里确认lower_case_table_names的设置,Linux 下默认区分大小写,容易踩。
6. 进阶玩法:把状态机日志变成你的排查利器
跑通之后,我建议你做的第一件事不是加功能,而是把「状态变更日志」补全。这套源码里订单状态变更有日志表,但字段往往只记了「从什么状态到什么状态」,没记「谁触发的、什么时间、关联的请求号」。补上这三样,你排查线上问题时就不是在黑匣子里猜,而是能顺着一条订单的时间线还原整个过程。
具体做法是在每次状态变更的地方统一走一个方法,而不是散落各处直接 update:
// 统一状态变更入口,强制写日志 public void changeStatus(Long orderId, int from, int to, String operator, String requestId) { int rows = orderMapper.updateStatus(orderId, from, to); if (rows == 0) { throw new BizException("状态已变更,请刷新后重试"); } statusLogMapper.insert(new StatusLog(orderId, from, to, operator, requestId, new Date())); }参数说明:from是前置状态,作为更新条件防止并发覆盖;operator区分是乘客、司机还是系统触发;requestId串起同一次请求的所有日志,排查时一个 id 就能捞出全链路。这个改造成本很低,但收益极高,尤其是订单量上来之后,没有这条时间线,你连「订单为什么取消」都说不清。
验证方法也简单:造一条订单,走完下单、接单、行程、支付全流程,然后按orderId查日志表,看状态是不是严格按 1→2→3→4→5→6 走,中间有没有跳变或重复。再故意并发接单,看日志里是不是只有一个司机成功。这套自检走一遍,你对这份源码的掌握程度就超过大多数只跑了个 demo 的人了。
从那以后我每次拆这类业务源码,都强制自己先把状态变更日志补全再动业务逻辑,因为后悔药从来不是事后补日志能买到的。希望这份拆解帮到你,拿到包之后先读状态机、再跑链路、最后补日志,顺序别乱。
本文还有配套的精品资源,点击获取