
代驾系统源码这五个字在各大代码仓库和资源站上一搜能出来几百个结果但真正把订单从呼叫跑到支付闭环的项目屈指可数。我自己这两年用Java Web技术栈做过、也帮人改过几版代驾管理系统最深的感受是代驾系统这个题目难点从来不在CRUD而在于订单流转、派单并发、计费精度这三块。这篇文章不是贴个GitHub链接就完事而是把一套基于Web的Java代驾管理系统从需求拆解、表结构设计、核心接口实现到线上踩坑完整复盘一遍。适合正在做课程设计、毕业设计或者想用Spring Boot练手一个完整业务系统的同学参考。先说明一点网上流传的很多代驾系统源码其实是“司机信息管理系统”换了个标题。它们能做的极限是增删改查一个司机表外加一张看着像样的后台管理页面。真正的代驾系统核心是业务闭环——用户下单、司机抢单/派单、出发到达、计费结算、支付流水这条链路缺一环项目就没有实际参考价值。所以这套设计会围绕业务闭环展开而不是堆功能模块。1. 先想清楚代驾系统到底在管什么很多人拿到题目第一反应是列功能清单用户管理、司机管理、订单管理、评价管理、支付管理、公告管理……清单列完就开始建表写代码。这种做法不是不行但写着写着就会迷失——因为代驾的本质是一个实时履约平台它的核心矛盾是“用户有需求时附近恰好有合格司机可调度”所有功能都是围绕这个核心矛盾展开的。1.1 业务角色与订单闭环代驾系统里至少有四种角色写代码前必须分清各自的视角乘客端用户发起代驾需求看到订单状态变化完成支付。它最关心的是“车什么时候来、多少钱”。司机端接收新订单、抢单或接单、开始服务、结束服务。它最关心的是“单子多不多、离我远不远、价格合不合理”。管理后台审核司机入驻、管理费率规则、处理异常订单、查看平台运营数据。它最关心的是“业务量、违规率、资金流水是否对得上”。系统本身维护用户与司机之间的时空匹配保证同一订单不会被两个司机抢走保证计费结果准确可审计。整个订单生命周期可以浓缩成一张状态流转图但这里我不画图用文字描述清楚用户创建订单待接单→ 司机接单已接单/待出发→ 司机到达上车点并开始服务服务中→ 司机送达目的地并结束待支付→ 用户支付已完成任何环节都可能出现取消已取消。这个状态机是后面所有接口设计的锚点。1.2 两个最常见的理解误区第一个误区是“把代驾做成打车软件”。代驾和网约车的最大区别在于网约车是从A到B的运输服务而代驾是“人和车一起从A到B”——司机需要先到达乘客位置然后驾驶乘客的车送乘客回家。这意味着系统必须跟踪两个位置司机位置和订单起点位置派单时计算的是“司机距离乘客的距离”而不是“乘客当前位置到目的地距离”。第二个误区是“计费就是起步价加里程费”。真实代驾场景的计费维度至少包括起步价、超里程费、夜间服务费、等待费、动态调价不同城市规则差异极大。把费率硬编码在Java代码里是这个项目最常见的败笔后面我会专门讲怎么用一张费率表解决这个问题。2. 技术选型什么样的Java Web组合最稳代驾系统是一个典型的业务系统不是一个技术炫技场。选型的第一原则是“自己Hold得住”第二原则是“能承载业务并发”第三原则才是“技术栈看起来高级”。2.1 技术栈与选型理由我用的这套组合在课程设计和中小企业项目中都是非常成熟的搭配组件选型理由开发框架Spring Boot 2.7.x生态成熟、资料多、自动配置省心适合快速搭建web项目ORMMyBatis-Plus单表CRUD不用写SQL复杂查询手写XML折中得很舒服数据库MySQL 8.0关系型数据存储的标配事务支持可靠缓存Redis用户会话、司机定位、抢单锁、热点数据缓存都靠它认证方案JWT Spring Interceptor无状态、易扩展适合前后端分离web项目构建工具Maven主流中的主流团队协作、依赖管理都方便地图服务高德/百度Web服务API不重复造轮子用现成的逆地址解析和距离测量接口为什么不用Spring Cloud微服务因为代驾系统的核心业务规模单体应用完全能扛住。引入微服务等于给自己增加服务注册、配置中心、网关、分布式事务一堆复杂度对学习项目来说纯属劝退。真到了需要拆分的时候按照订单、用户、支付三个领域拆成三个Spring Boot模块也比一开始就上全家桶稳妥。2.2 工程模块划分一个好的Maven工程结构能让代码维护者少骂几句。dai-jia-system ├── dai-jia-common // 公共模块统一返回体、异常、工具类 ├── dai-jia-admin // 管理后台接口模块 ├── dai-jia-api // 乘客端与司机端接口模块 ├── dai-jia-service // 业务逻辑层 ├── dai-jia-mapper // MyBatis映射层 └── dai-jia-web // 启动入口与配置实际操作中我会把Controller、Service、Mapper严格分层但Entity和DTO分开——数据库实体绝不允许直接暴露给前端。否则一旦表结构微调接口响应跟着变前端联调和后续维护都会非常痛苦。3. 数据库设计几张核心表和它们的字段代驾系统的表不算多但每张表的字段设计都直接影响业务实现难度。下面这五张表是整套系统的骨架我会把关键字段和设计意图讲清楚。3.1 订单表系统的核心容器订单表是代驾系统的中枢几乎所有业务动作都会落到订单状态的更新上。CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号业务唯一, user_id BIGINT NOT NULL COMMENT 下单用户ID, driver_id BIGINT DEFAULT NULL COMMENT 接单司机ID未接单时为空, order_type TINYINT NOT NULL DEFAULT 1 COMMENT 1实时单 2预约单, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待接单 2已接单 3服务中 4待支付 5已完成 6已取消, start_lng DECIMAL(10, 6) NOT NULL COMMENT 起点经度, start_lat DECIMAL(10, 6) NOT NULL COMMENT 起点纬度, start_addr VARCHAR(255) NOT NULL COMMENT 起点地址描述, end_lng DECIMAL(10, 6) DEFAULT NULL COMMENT 终点经度, end_lat DECIMAL(10, 6) DEFAULT NULL COMMENT 终点纬度, end_addr VARCHAR(255) DEFAULT NULL COMMENT 终点地址描述, estimated_distance DECIMAL(10, 2) DEFAULT NULL COMMENT 预估里程公里, estimated_amount DECIMAL(10, 2) DEFAULT NULL COMMENT 预估金额, real_distance DECIMAL(10, 2) DEFAULT NULL COMMENT 实际里程公里, real_amount DECIMAL(10, 2) DEFAULT NULL COMMENT 实际金额, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付, cancel_source TINYINT DEFAULT NULL COMMENT 取消方1用户 2司机 3系统, cancel_reason VARCHAR(255) DEFAULT NULL COMMENT 取消原因, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, grab_time DATETIME DEFAULT NULL COMMENT 接单时间, start_time DATETIME DEFAULT NULL COMMENT 开始服务时间, finish_time DATETIME DEFAULT NULL COMMENT 结束服务时间, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_driver_id (driver_id), KEY idx_status_create (status, create_time), UNIQUE KEY uk_order_no (order_no) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 代驾订单表;几个容易被忽略的细节order_no一定要独立于自增主键。自增ID会暴露平台单量也容易被人遍历抓取业务编号加上日期和随机数既美观又安全。status和create_time必须建联合索引因为管理后台最多的查询就是“某段时间内的订单状态分布”。经度纬度用DECIMAL(10,6)而不是DOUBLE避免浮点精度误差距离计算时再转成BigDecimal使用。3.2 用户表、司机表与审核状态用户表和司机表的结构高度相似但司机表多了审核字段和位置字段因为司机是需要平台背书的履约主体。CREATE TABLE t_driver ( id BIGINT PRIMARY KEY AUTO_INCREMENT, driver_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18) NOT NULL COMMENT 身份证号展示时需脱敏, driver_license VARCHAR(20) NOT NULL COMMENT 驾驶证号, car_plate VARCHAR(10) NOT NULL COMMENT 车牌号, car_brand VARCHAR(50) DEFAULT NULL, audit_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, online_status TINYINT NOT NULL DEFAULT 0 COMMENT 0离线 1在线 2忙碌, latitude DECIMAL(10, 6) DEFAULT NULL COMMENT 司机实时纬度, longitude DECIMAL(10, 6) DEFAULT NULL COMMENT 司机实时经度, rating DECIMAL(2, 1) DEFAULT 5.0 COMMENT 评分, total_orders INT DEFAULT 0 COMMENT 累计完单数, balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT 账户余额, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone), KEY idx_location (latitude, longitude), KEY idx_online_status (online_status) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 司机信息表;这里要特别提醒身份证、驾驶证这类敏感信息项目里可以做加密存储展示给前端时用工具类脱敏只保留前三位和后四位。这在毕设答辩里是个很好的加分点也体现开发者的安全意识。3.3 费率表计费灵活性的关键代驾费率因城市而异甚至同一个城市不同时段价格都不一样。如果费率在Java代码里用if-else写改一次规则就要发一次版这在真实业务中是不可接受的。CREATE TABLE t_fee_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, city_code VARCHAR(20) NOT NULL COMMENT 城市编码, city_name VARCHAR(50) NOT NULL COMMENT 城市名称, base_price DECIMAL(10, 2) NOT NULL COMMENT 起步价, base_km DECIMAL(4, 1) NOT NULL COMMENT 起步包含里程公里, price_per_km DECIMAL(10, 2) NOT NULL COMMENT 超出起步里程后的单价元/公里, night_start VARCHAR(5) NOT NULL DEFAULT 22:00 COMMENT 夜间的开始时间, night_end VARCHAR(5) NOT NULL DEFAULT 06:00 COMMENT 夜间的结束时间, night_price_per_km DECIMAL(10, 2) NOT NULL COMMENT 夜间单价, wait_price_per_min DECIMAL(10, 2) DEFAULT 1.00 COMMENT 等待费元/分钟, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_city (city_code) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 代驾费率规则表;有了这张表新增一个城市的计费规则或者调整某个时段的夜间单价管理后台改一条记录就行完全不用动代码。这也是“基于Web的代驾管理系统”该有的样子——业务人员能自助配置规则而不是每次改规则都找开发。3.4 定位轨迹表知道车从哪来、到哪去订单结束后的里程核对和纠纷处理都依赖轨迹数据。我一直坚持用一张独立的轨迹表而不是简单在订单表里存两个坐标。CREATE TABLE t_order_track ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, lng DECIMAL(10, 6) NOT NULL, lat DECIMAL(10, 6) NOT NULL, track_time DATETIME NOT NULL COMMENT 定位上报时间, KEY idx_order_time (order_id, track_time) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 订单轨迹记录表;司机端每5秒上报一次定位按每单平均40分钟算一单大约产生480条轨迹数据。记录量不小但考虑到这是学习项目单表完全够用。如果数据量大后续可以按order_id做分表或者引入时序数据库这是后话。4. 订单流程实现从用户呼叫到司机接单订单模块是整个系统的发动机。我建议按“状态机 核心接口 并发处理”三层来组织代码而不是把所有逻辑塞进一个Service。4.1 状态机先把能走的路都画出来在写第一个方法之前先用枚举把订单状态和允许的迁移关系定死public enum OrderStatus { WAITING(1, 待接单), GRABBED(2, 已接单), IN_SERVICE(3, 服务中), WAIT_PAY(4, 待支付), FINISHED(5, 已完成), CANCELED(6, 已取消); public final int code; public final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } }然后写一个状态流转校验器。这不是花架子它能防止用户端调接口乱改状态也能防止并发场景下重复操作。比如一个司机在接单前订单必须处于“待接单”状态状态不对就直接抛业务异常。4.2 下单接口用户发起代驾需求用户端最核心的接口是创建订单。这个接口的输入参数一般包括起点经纬度、起点地址、终点经纬度、终点地址、预约时间可选。创建订单的逻辑顺序是这样的校验用户是否登录是否被拉黑黑名单这种功能可视情况省略。接收定位参数调地图服务的逆地址编码接口把经纬度转成可读地址。根据起点的城市编码查费率表计算预估金额。生成唯一订单号插入订单表状态为“待接单”。往Redis里写入一个“待关单”的key比如order:timeout:{orderNo}设置10分钟过期过期后由定时任务把还没被接的单子自动取消。第5步是真实业务里很关键的一环。用户下了单但一直没司机接不能让他无限期等下去。用Redis的过期key加定时扫描比Java里的ScheduledExecutorService更优雅也方便多实例部署时统一管理。4.3 派单策略距离优先的主动推荐 vs 附近司机的抢单代驾行业存在两种主流派单模式抢单模式订单创建后推送给附近司机司机主动抢单。这种模式实现简单、司机参与感强但对平台来说不够公平——手速快的司机永远抢到好单老司机反而接不到。指派模式系统根据距离、评分、完单量计算一个综合得分把订单派给最合适的司机。司机可以选择接受或拒绝。这种模式效率更高但实现复杂度也更高。我的建议是两种都做但第一版先把“附近司机抢单”跑通再考虑指派。抢单接口的伪代码如下Transactional public boolean grabOrder(Long orderId, Long driverId) { int updated orderMapper.updateStatusIfWaiting(orderId, driverId); if (updated 0) { throw new BusinessException(手慢了订单已被抢走); } // 设置司机状态为忙碌 driverService.setBusy(driverId); // 清除关单定时任务 redisTemplate.delete(order:timeout: orderId); return true; }关键就在updateStatusIfWaiting这条SQL上UPDATE t_order SET driver_id #{driverId}, status 2, grab_time NOW() WHERE id #{orderId} AND status 1这种“基于乐观锁的带条件更新”是并发抢单最可靠、最不容易出问题的方案。比先用select查状态、再update的“先查后改”方式强太多了因为后一种在并发下一定会产生重复接单的脏数据。5. 计费与结算最容易暴露功底的一块计费关系到真金白银如果设计得乱后面改都改不动。我的经验是预估金额下单时算实际金额结束时算每一笔都要留痕。5.1 计费规则拆解起步价、里程费、夜间费怎么组合以某个城市的规则为例起步价30元含10公里。超出起步里程后每公里5元。夜间22:00至次日06:00每公里加收2元。等候费每分钟1元从司机到达后开始计算超过10分钟后开始计费。动态调价雨天、高峰期平台可以上浮1.2倍。在实际代码里先查询城市费率表然后根据服务时间和里程分三段算清楚public BigDecimal calculate(Order order, FeeRule rule, boolean isNight, int waitMinutes) { BigDecimal amount rule.getBasePrice(); // 超出起步里程的部分 if (order.getRealDistance() rule.getBaseKm()) { BigDecimal extraKm order.getRealDistance().subtract(rule.getBaseKm()); if (isNight) { amount amount.add(extraKm.multiply(rule.getNightPricePerKm())); } else { amount amount.add(extraKm.multiply(rule.getPricePerKm())); } } // 等待费 if (waitMinutes 10) { amount amount.add(BigDecimal.valueOf(waitMinutes - 10) .multiply(rule.getWaitPricePerMin())); } // 四舍五入保留两位向上取整到元 return amount.setScale(0, RoundingMode.UP); }用BigDecimal而不是double是这条代码里的底线。浮点数在金额计算里的精度问题懂得都懂线上金额对不上账的时候再回头改就晚了。5.2 费用确认与结算流水司机点击“结束服务”时系统根据起点终点距离调用地图接口算实际里程和费率算实际金额更新订单表状态为“待支付”并写入一条支付流水记录。CREATE TABLE t_payment_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, payment_no VARCHAR(32) NOT NULL COMMENT 支付流水号, order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10, 2) NOT NULL, pay_type TINYINT NOT NULL COMMENT 1余额 2微信 3支付宝, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1成功 2失败, third_party_no VARCHAR(64) DEFAULT NULL COMMENT 第三方支付流水号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, UNIQUE KEY uk_payment_no (payment_no), KEY idx_order_id (order_id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 支付流水表;支付这里如果只是做课程设计不建议真的接入微信支付、支付宝支付那会涉及商户号申请和复杂的回调逻辑。用一个模拟支付的流程用户点击支付系统生成支付流水直接调用一个Mock支付接口把流水状态置为“成功”订单状态从“待支付”更新为“已完成”。但流水表一定要设计成能接第三方支付的样子方便以后对接。6. 实测踩坑并发、定位与数据一致性接下来分享我在跑通这套系统过程中实际遇到的问题和排查链路。这些坑在正常参考源码里很难看到但每一个都可能让你卡上一天。6.1 抢单并发为什么Redis锁不是万能的一开始我直接用Redis的setnx命令实现抢单锁Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:grab: orderId, driverId, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 执行抢单 }这个方案的漏洞在于锁的粒度是整个订单但真正需要保证的其实是“订单状态的原子迁移”。如果某个司机拿到锁后执行逻辑很慢锁超时被释放另一个司机又拿到锁进来同一个订单还是可能被处理两次。后来我把方案改成了“数据库乐观锁为主Redis锁为辅”用update ... where status 1作为最终的确定性屏障。Redis锁用来做提前过滤减少无效请求打到数据库层但绝不依赖它做最终一致性的保证。这个思路在很多并发业务场景里都适用缓存、锁、队列都只是提升性能或削峰的手段数据库的行锁和条件更新才是最后一道防线。6.2 定位数据漂移司机明明在A却显示在B司机端上报定位是定时轮询但如果司机在地下车库、高架桥下GPS信号漂移很严重。我在测试时就遇到过司机已经到乘客楼下了系统还显示他在一公里外的商场。解决方案是在司机端上报定位时加一层“真实性校验”如果新坐标与上一次坐标距离超过2公里且时间间隔小于10秒判定为无效定位丢弃。如果连续多次定位漂移将司机在线状态标记为“位置可疑”派单权重降低。距离计算用Haversine公式这是地图场景最常用的球面距离算法public static double distance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double a radLat1 - radLat2; double b Math.toRadians(lng1) - Math.toRadians(lng2); double s 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); return s * 6371.0088; }这里用6371.0088作为地球半径均值计算出的距离单位是公里和地图服务商的计算结果误差在可接受范围内。6.3 数据一致性订单已支付司机却说没收到钱这是一个很典型的分布式业务一致性问题。用户支付成功后系统需要做三件事更新订单状态、更新支付流水状态、给司机账户加钱。这三件事如果只靠一个本地事务解决在真实分布式环境下是不够的。在单体应用里这三步确实可以通过一个Transactional搞定。但要注意的是如果需要调用外部接口比如发短信通知司机、调用地图API生成电子发票就绝不能把外部调用放在事务里——否则事务长时间不提交数据库连接池被耗尽系统直接雪崩。我的做法是本地事务只负责更新数据库状态。事务提交成功后通过Spring的事件机制发布一个“支付成功”事件。后续发短信、发通知、给司机账户加余额都在事件监听器里异步执行。Transactional public void paySuccess(PaymentFlow flow) { paymentFlowMapper.updateStatus(flow.getId(), PaymentStatus.SUCCESS); orderMapper.updatePayStatus(flow.getOrderId(), PayStatus.PAID); // 事务提交后发布事件 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { applicationEventPublisher.publishEvent(new PaySuccessEvent(flow)); } }); }6.4 预约单超时没有状态回收机制系统上线后我发现一个问题预约单如果司机一直没有接订单会永远卡在“待接单”。后来加了一个定时任务每1分钟扫描一次把超过30分钟还没司机接的预约单自动置为“已取消”并给用户发一条短信通知。这个功能看似不起眼但没有它系统的订单表会积累大量僵尸订单管理后台的统计数字也会失真。7. 管理后台从会“玩代码”到会“管业务”的分水岭很多代驾系统源码的管理后台只有一个用户列表和订单列表但真正的管理后台至少要解决三个问题审核司机入驻、配置费率规则、查看订单异常。7.1 司机入驻审核司机注册不等于司机可以接单必须经过平台审核。审核功能的核心是一张带状态的司机表管理员看到的是“待审核列表”点击通过后司机才能看到接单大厅。这个功能本身不复杂但体现了一个重要的业务思想代驾平台的司机是服务供给方供给方的质量直接影响用户体验。有审核环节你的系统才是一个可信赖的平台而不是一个单纯的C2C信息中介。7.2 订单检索与统计管理后台的订单列表需要支持多维筛选按订单号精确查、按用户手机号模糊查、按司机手机号模糊查、按订单状态查、按时间范围查。这些筛选条件如果都在代码里用if拼SQL效率低下且容易出错。用MyBatis-Plus的QueryWrapper做动态条件拼接清爽很多LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(orderNo), Order::getOrderNo, orderNo) .eq(driverId ! null, Order::getDriverId, driverId) .between(startTime ! null endTime ! null, Order::getCreateTime, startTime, endTime) .eq(status ! null, Order::getStatus, status) .orderByDesc(Order::getCreateTime);7.3 基于数据统计的简单运营看板不需要ECharts那一大套一个简单的统计接口返回几个数字就好今日订单总量、今日完单量、今日交易总额、司机在线数。如果时间充裕再加一张按小时统计的单量折线图数据。这样一来管理后台就不再是“好看的摆设”而是真正能辅助运营决策的工具这在答辩或述职时是很加分的。8. 从“能跑”到“能用”你还应该考虑这些扩展整套系统跑通之后我复盘发现还有一些点值得继续做它们决定了这个项目是停留在“课设水平”还是“接近真实工业级项目”。8.1 司机端位置实时推送用WebSocket还是轮询第一版我用的是前端定时2秒一次调用“获取附近订单”接口这种方式实现简单但有两个问题实时性不够而且每个司机每隔几秒刷一次单机并发一上来数据库就扛不住。更好的方案是用WebSocket建立长连接司机端上线时把连接通道注册到Redis订单创建后系统通过WebSocket把新订单信息推送给附近在线的司机。这套机制比定时轮询优雅得多而且能作为项目亮点写进简历。顺序是这样的司机上线 → 系统推送附近订单 → 司机点击抢单 → 抢单结果通过WebSocket实时返回。整个流程一气呵成用户体验也是“秒级”的。8.2 订单取消的补偿逻辑取消订单不是简单改个状态就完事。分几种情况司机接单前用户取消直接取消无任何影响。司机接单后、到达前用户取消司机会有意见可能需要给司机发放补偿券。司机接单后未到达就点击“开始服务”又立刻点“结束服务”骗里程费这是平台风控需要识别的。我当时给Driver的取消逻辑加了“距接单时间小于1分钟”的校验小于1分钟不允许取消避免恶意取消影响司机接单体验。这种边界情况的处理往往是面试官最喜欢追问的地方也最能体现你的系统思考能力。8.3 定位服务的抽象与接口设计千万不要在Service代码里直接调用高德地图API。地图厂商的SDK经常升级参数和返回结构都可能变如果调用逻辑散落在各个业务方法里升级时就是灾难。我的做法是定义一层MapService接口public interface MapService { GeoPoint parseAddress(String address); // 地址转经纬度 String reverseGeo(double lat, double lng); // 经纬度转地址 double getDistance(double lat1, double lng1, double lat2, double lng2); // 距离测量 }以后想换百度地图或者腾讯地图只需要写一个新的实现类并替换配置业务层的代码完全不用动。这种“面向接口编程”的习惯在真实项目开发里比掌握了多少框架API都重要。9. 给新手的最后提醒从哪一步开始动手如果你的最终目标是完成一个能演示、能答辩的基于Web的代驾系统我的建议是不要一上来就追求大而全。我自己的经验是分四步走第一周搞定数据库设计和订单状态机模型先把订单创建、接单、完成这个主链路跑通。第二周把司机端定位上报和附近订单查询做了这是代驾和普通CRUD系统的最大区别。第三周实现计费规则确保预估金额和实际金额两条计算逻辑一致。第四周补管理后台和演示数据并针对答辩可能问的问题准备技术说明。顺序千万不能反。我见过太多人花了两周做后台的富文本编辑器结果核心的订单流转没有跑通演示时一接单就报错这种翻车现场实在让人心疼。另外强烈建议把项目跑起来之后用自己的手机号注册一个乘客账号再注册一个司机账号后台审核通过完整走一遍“下单-接单-开始服务-结束服务-支付”的流程把流程截图或录屏保存下来。这既是项目日志也是你未来演示和答辩时的素材。纸上得来终觉浅这两年代驾相关的项目我重复搭了很多遍每一遍都能发现上一版的设计漏洞——数据库漏了索引、状态机少了一种迁移、计费算错了夜间时段。这套Java Web代驾系统的实现逻辑以上就是我最完整的一版复盘。如果你在按这个思路写代码时遇到了具体的问题比如某个表字段拿不准、某个接口并发有问题随时可以照着上面的设计调整。多跑几遍真实流程你会发现“能跑”和“能用”之间的距离恰恰是这些细节填平的。