简介:这份资源是飞滴网约车项目 online-taxi-public 的完整源码包,面向希望深入理解网约车平台架构的 Java 开发者与后端学习者。项目围绕在线打车核心业务展开,涵盖用户注册登录、位置定位与地图集成、车辆与司机管理、订单匹配与计费、支付结算、异常处理及安全合规等模块,适合研究分布式系统、高并发与实时数据处理场景。压缩包共 163 个文件,以 132 个 Java 源码为主体,辅以 19 个 XML 配置、9 个 YML 配置文件及少量说明文档,整体约 137KB,结构紧凑便于按模块阅读。内容预览可见订单、价格规则、验证码、司机车辆绑定等核心服务类,能帮助读者梳理业务分层与接口设计思路。目前已有 689 人学习下载,适合作为网约车业务建模与后端工程实践的参考案例。
1. 从一份网约车源码说起:为什么我建议你亲手跑一遍 online-taxi-public
如果你手头正好有一份online-taxi-public.zip,别急着解压完就丢进 IDE 点运行。我见过太多人拿到网约车类项目,第一反应是“这不就是个打车 Demo 吗”,结果卡在数据库连不上、地图 Key 没配、订单状态流转对不上,最后草草删库跑路。这个项目真正值得跑一遍的原因,不在于它复刻了某个 App 的界面,而在于它把“乘客下单 → 司机接单 → 行程计费 → 订单完结”这条核心链路完整地串了起来,并且把派单、计价、状态机这些网约车业务里最容易写成一团乱麻的逻辑,拆成了可以单独调试的模块。它适合两类人:一类是想找一个业务复杂度够、但又不至于庞大到读不完的 Java 后端项目练手的新手;另一类是想看看“地理位置 + 订单状态 + 并发抢单”这三件事在工程上怎么落地的熟手。接下来我不讲虚的,直接按“这项目能干什么 → 怎么跑起来 → 核心代码长什么样 → 怎么改”的顺序,把它拆开给你看。
2. 项目定位与技术选型:它到底是不是一个能写进简历的网约车后端
2.1 业务边界:只做“打车闭环”,不做全家桶
拿到源码先看它不做什么,比看它做什么更重要。这个项目没有去碰支付清结算、没有做复杂的司机端 UI 动画、也没有接入真实的地图导航 SDK 做路径规划。它的边界很清晰:围绕“订单”这个聚合根,把乘客发单、系统派单、司机接单、行程开始、行程结束、费用结算这几个状态串起来。你会在代码里看到大量的状态枚举和状态流转判断,而不是一堆花哨的页面。这种设计的好处是,你调试的时候只需要盯住一个订单 ID,就能把整条链路走完,不用在十几个微服务之间来回跳。常见做法是,这类练手项目会把“计价规则”做成可配置的策略模式,而不是硬编码在 Service 里,这一点在后面的核心模块拆解里会看到。
2.2 技术栈拆解:Spring Boot + MyBatis-Plus + Redis + MySQL 的组合拳
从目录结构和依赖文件来看,这是一个典型的 Spring Boot 单体应用,分层方式是 Controller → Service → Mapper。数据库用 MySQL 存订单、乘客、司机等核心数据,Redis 主要承担两个角色:一是缓存司机的地理位置(GEO 结构),二是做订单 ID 的分布式发号器或者简单的锁。我一般会先看pom.xml或者build.gradle来确认版本,但这里要提醒一句,不同版本的 Spring Boot 对 JDK 要求不一样,如果你本地是 JDK 17,而项目用的是 Spring Boot 2.x,启动时可能会遇到反射相关的警告甚至报错,这个在避坑章节会细说。技术选型上没有用 Spring Cloud 那一套,说明作者是想让你聚焦业务逻辑,而不是被服务注册发现、配置中心这些基础设施分散精力。对于二次开发来说,这反而是好事,你可以很轻松地把某个模块抽出来改成微服务,而不用先拆一堆依赖。
2.3 目录结构速览:先找入口,再找核心
解压后不要急着点运行,先花五分钟把目录扫一遍。通常你会看到类似这样的结构:
online-taxi-public/ ├── src/main/java/com/example/taxi/ │ ├── controller/ # 接口层,处理 HTTP 请求 │ ├── service/ # 业务逻辑,订单状态流转在这里 │ ├── mapper/ # MyBatis 接口,对应 XML 或注解 SQL │ ├── entity/ # 数据库实体类 │ ├── dto/ # 数据传输对象,用于接口出入参 │ ├── config/ # Redis、MyBatis、Web 配置 │ └── common/ # 统一返回结果、常量、工具类 ├── src/main/resources/ │ ├── application.yml # 数据库、Redis 连接配置 │ ├── mapper/ # MyBatis XML 文件(如果有) │ └── static/ # 可能包含一个简单的测试页面 └── sql/ # 建表语句,通常在这里重点看sql/目录,里面一般会有init.sql或者schema.sql,这是你跑起来的第一步。如果缺少建表语句,项目是跑不起来的,这时候你可以根据entity类里的字段反推建表,但更稳妥的做法是去翻一下README或者doc目录,通常作者会把初始化脚本放在那里。另外,application.yml里的数据库密码、Redis 地址都是占位符,你需要改成自己本地的配置,这个后面会讲。
3. 跑起来才算数:从零启动 online-taxi-public 的完整操作路径
3.1 环境准备:JDK、Maven、MySQL、Redis 一个都不能少
在开始之前,先确认你本地有这些环境:JDK 8 或 11(具体看项目编译版本,可以在pom.xml的<java.version>里看到),Maven 3.6+,MySQL 5.7 或 8.0,Redis 5.0+。我习惯用 Docker 起 MySQL 和 Redis,这样不污染本机环境,命令如下:
# 启动 MySQL,映射到本机 3306 docker run -d --name taxi-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0 # 启动 Redis,映射到本机 6379 docker run -d --name taxi-redis -p 6379:6379 redis:6.2启动后,用客户端连上 MySQL,创建一个名为taxi的数据库,字符集用utf8mb4。然后把项目sql/目录下的建表语句执行一遍。注意,有些建表语句里会写CREATE DATABASE IF NOT EXISTS taxi,如果没有,你就手动建。Redis 不需要额外操作,默认无密码即可,如果项目配置了密码,你需要在application.yml里同步改掉。
3.2 配置修改:数据库连接、Redis 地址与端口
打开src/main/resources/application.yml,你会看到类似这样的配置:
spring: datasource: url: jdbc:mysql://localhost:3306/taxi?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 database: 0这里有几个坑点:第一,serverTimezone必须设置,否则 MySQL 8.0 会报时区错误;第二,如果你的 MySQL 密码不是123456,记得改;第三,Redis 的database默认是 0,如果你本地 Redis 已经有其他数据,建议改成 1 或者 2,避免键冲突。改完配置后,不要急着启动,先检查一下pom.xml里的依赖是否都下载完整,可以执行mvn clean compile看看有没有编译错误。
3.3 启动与验证:看日志、调接口、查数据库
在项目根目录执行mvn spring-boot:run,或者用 IDE 直接运行主启动类。启动成功后,控制台会打印Started TaxiApplication in x.xxx seconds。这时候别急着高兴,先看日志里有没有 Redis 连接失败的警告。如果一切正常,你可以用 Postman 或者 curl 调一下乘客下单接口。假设接口是POST /api/order/create,请求体大概长这样:
{ "passengerId": 1001, "startAddress": "某地铁站A口", "endAddress": "某写字楼B座", "startLongitude": 116.397, "startLatitude": 39.908 }调用后,去数据库order表里查一下,应该能看到一条新订单,状态是“待接单”。然后再调司机接单接口,把订单状态改成“已接单”。如果这两步都成功了,说明核心链路已经通了。这时候你可以打开 Redis 客户端,执行GEOPOS driver_location 司机ID,看看司机的位置有没有被缓存进去。这一步验证很重要,因为很多派单逻辑依赖 Redis 的 GEO 查询,如果位置没存进去,后面派单会一直返回空。
4. 核心模块拆解:订单状态机、派单逻辑与计价策略的代码实现
4.1 订单状态流转:一个枚举类撑起整个业务闭环
网约车业务最怕的就是状态乱跳,比如订单还没接单就直接变成“已完成”。这个项目里通常会有一个OrderStatusEnum,把状态定义清楚:
public enum OrderStatusEnum { WAITING_ACCEPT(1, "待接单"), ACCEPTED(2, "已接单"), DRIVER_ARRIVED(3, "司机已到达"), IN_TRIP(4, "行程中"), COMPLETED(5, "已完成"), CANCELLED(6, "已取消"); private final int code; private final String desc; OrderStatusEnum(int code, String desc) { this.code = code; this.desc = desc; } // 根据 code 获取枚举,常用于数据库查询后转换 public static OrderStatusEnum fromCode(int code) { for (OrderStatusEnum status : values()) { if (status.code == code) { return status; } } throw new IllegalArgumentException("未知订单状态: " + code); } // 判断当前状态是否可以流转到目标状态 public boolean canTransitionTo(OrderStatusEnum target) { switch (this) { case WAITING_ACCEPT: return target == ACCEPTED || target == CANCELLED; case ACCEPTED: return target == DRIVER_ARRIVED || target == CANCELLED; case DRIVER_ARRIVED: return target == IN_TRIP || target == CANCELLED; case IN_TRIP: return target == COMPLETED; default: return false; } } }这段代码的关键在于canTransitionTo方法,它把状态流转规则收拢在一个地方,而不是散落在各个 Service 里。你改业务的时候,只需要改这个枚举,不用去翻十几个文件。参数说明:code是存数据库的值,desc是给前端展示的文案。常见做法是,在 Service 层更新订单状态前,先调用canTransitionTo校验,如果返回 false 就直接抛业务异常,避免脏数据写进数据库。
4.2 派单逻辑:基于 Redis GEO 的附近司机查询
派单的核心是“找到离乘客最近的空闲司机”。这个项目里一般会用 Redis 的 GEO 结构来存司机位置,查询时用GEORADIUS或者GEOSEARCH。代码大概长这样:
@Service public class DispatchService { @Autowired private StringRedisTemplate redisTemplate; private static final String DRIVER_GEO_KEY = "driver:location"; /** * 查询乘客附近指定半径内的司机 * @param longitude 乘客经度 * @param latitude 乘客纬度 * @param radius 搜索半径,单位公里 * @return 司机ID列表,按距离从近到远排序 */ public List<Long> findNearbyDrivers(double longitude, double latitude, double radius) { Circle circle = new Circle(longitude, latitude, radius, Metrics.KILOMETERS); GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo() .radius(DRIVER_GEO_KEY, circle, RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs() .includeDistance() .sortAscending() .limit(10)); List<Long> driverIds = new ArrayList<>(); if (results != null) { for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) { driverIds.add(Long.parseLong(result.getContent().getName())); } } return driverIds; } }逻辑说明:radius方法接收圆心和半径,返回按距离升序排列的司机列表,limit(10)表示最多取 10 个,避免一次拉太多数据。参数上,radius一般设 3 到 5 公里,具体看城市密度。这里有个细节:司机的位置不是一成不变的,通常司机会每隔几秒上报一次位置,后端用opsForGeo().add()更新。如果你发现派单总是返回空,先检查司机位置有没有写进 Redis,再检查经纬度顺序是不是写反了——Redis GEO 要求先经度后纬度,写反了会查到地球另一端去。
4.3 计价策略:用策略模式把“起步价 + 里程费 + 时长费”拆开
计价是网约车最容易写成一坨 if-else 的地方。这个项目里一般会定义一个PricingStrategy接口,然后针对不同车型或者不同时段实现不同的策略:
public interface PricingStrategy { BigDecimal calculate(BigDecimal distanceKm, int durationMinutes); } @Component public class StandardPricingStrategy implements PricingStrategy { private static final BigDecimal BASE_PRICE = new BigDecimal("10.00"); private static final BigDecimal PER_KM_PRICE = new BigDecimal("2.50"); private static final BigDecimal PER_MINUTE_PRICE = new BigDecimal("0.50"); @Override public BigDecimal calculate(BigDecimal distanceKm, int durationMinutes) { BigDecimal distanceFee = distanceKm.multiply(PER_KM_PRICE); BigDecimal timeFee = BigDecimal.valueOf(durationMinutes).multiply(PER_MINUTE_PRICE); return BASE_PRICE.add(distanceFee).add(timeFee).setScale(2, RoundingMode.HALF_UP); } }这段代码把计价公式封装在具体策略里,Service 层只需要根据订单类型选择对应的策略 Bean 即可。参数说明:distanceKm是行程里程,durationMinutes是行程时长,最终结果保留两位小数。如果你要加“夜间加价”或者“高峰溢价”,只需要新增一个策略实现类,然后在工厂里根据时间段返回不同的策略,不用动原来的代码。这就是策略模式的好处,也是我在二次开发时最推荐你保留的设计。
5. 避坑与常见问题:启动、派单、状态流转中容易翻车的地方
5.1 启动报错“Communications link failure”:MySQL 连不上
现象:启动时控制台抛异常,提示Communications link failure。原因通常是 MySQL 没启动,或者application.yml里的 URL、用户名、密码不对。解决:先用docker ps确认容器在运行,然后用mysql -h127.0.0.1 -uroot -p手动连一下,确认密码正确。如果 MySQL 是 8.0,还要检查serverTimezone是否配置,否则会报时区错误。我一般会在 URL 后面加上&allowPublicKeyRetrieval=true&useSSL=false,避免 SSL 握手失败。
5.2 派单接口返回空列表:Redis GEO 里没有司机数据
现象:调用派单接口,返回的司机列表是空的。原因有两个:一是司机位置根本没写进 Redis,二是查询半径太小。解决:先打开 Redis 客户端,执行ZCARD driver:location,如果返回 0,说明没有司机位置数据,你需要先调司机位置上报接口。如果返回大于 0,但查询还是空,就把半径从 3 公里改成 10 公里试试。另外注意经纬度顺序,GEOADD的格式是GEOADD key 经度 纬度 成员,写反了会查到南极去。
5.3 订单状态更新失败:状态机校验没通过
现象:调接口更新订单状态,返回“状态不允许流转”。原因是你跳过了中间状态,比如从“待接单”直接改成“行程中”。解决:先查一下当前订单的状态,然后按照OrderStatusEnum里定义的流转规则一步步来。如果你在二次开发时想加一个新状态,记得同步修改canTransitionTo方法,否则新状态永远无法到达。这个坑我踩过,当时加了个“司机迟到”状态,忘了改流转规则,调试了半天才发现。
5.4 接口返回时间格式不对:时区配置遗漏
现象:订单创建时间比实际时间少了 8 小时。原因:JVM 时区或者数据库连接时区没设对。解决:在application.yml的 JDBC URL 里加上serverTimezone=Asia/Shanghai,同时在启动类上加@PostConstruct设置TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"))。如果是 Spring Boot 2.x,还可以在配置文件里加spring.jackson.time-zone=GMT+8,保证 JSON 序列化也正确。
5.5 重复接单:并发下同一个订单被两个司机抢到
现象:两个司机同时点接单,结果都提示成功,数据库里订单的司机 ID 被覆盖了。原因:接单逻辑没有加锁,或者锁的粒度不对。解决:在接单的 Service 方法上加@Transactional,然后用UPDATE order SET driver_id = ? WHERE id = ? AND status = 1这种带状态条件的更新,通过数据库行锁保证只有一个司机能更新成功。如果返回影响行数为 0,说明订单已经被抢了,直接抛异常提示“手慢了”。这是网约车项目里最经典的并发问题,面试也常问。
6. 二次开发与改造思路:从跑通到改成你自己的项目
跑到这里,你已经能把这个项目启动起来,也能看到订单从创建到完成的完整链路。接下来如果你想把它改成自己的东西,我建议从三个方向入手。第一个方向是替换计价策略。现在的StandardPricingStrategy是写死的,你可以加一个NightPricingStrategy,然后在工厂里根据当前小时数返回不同的策略。改完之后,写一个单元测试,传入固定的里程和时长,断言价格符合预期。第二个方向是给派单加一层筛选。现在的派单只按距离排序,你可以加一个“司机评分”字段,在 Redis GEO 查询出附近司机后,再去数据库查他们的评分,按“距离 + 评分”加权排序。这个改动会涉及到多次查询,记得加缓存,否则 QPS 一上来数据库就扛不住了。第三个方向是把订单状态流转做成事件驱动。现在是在 Service 里直接改状态,你可以改成发布OrderStatusChangedEvent,然后由监听器去处理后续动作,比如发短信、更新司机统计。这样解耦之后,加新功能就不用改原来的代码了。
验证改动是否生效,我一般会写一个简单的集成测试:用@SpringBootTest启动上下文,然后模拟乘客下单、司机接单、行程结束,最后断言订单状态和价格。如果你不想写测试,也可以直接用 Postman 跑一遍接口,然后去数据库里核对数据。这里有个小技巧:把application.yml里的日志级别调到DEBUG,这样 MyBatis 会打印出执行的 SQL,你能清楚地看到每次状态更新到底改了哪些字段。最后说一个我自己的习惯:每次改完核心逻辑,我都会把订单表清空,重新从下单开始走一遍,确保没有脏数据影响判断。从那以后我每次动状态机或者计价相关的代码,都强制走一遍完整链路,再也不敢只测单个接口了。希望帮到你。
本文还有配套的精品资源,点击获取