网约车司机接单派单功能总结 📅 2026/7/3 1:59:07 一、功能整体链路这次功能不是单独做一个“接单接口”而是把乘客下单、司机在线、订单推送、司机抢单、拒单、订单状态同步都串成了一条完整链路1.乘客下单后order-srv创建订单订单状态为0表示待接单。2.下单成功后把订单消息发布到 RabbitMQ 的order_dispatch队列。3.driver-srv启动派单消费者消费order_dispatch消息。4.后端通过 WebSocket 把新订单实时推送给在线司机。5.司机端可以查看附近待接订单列表或地图订单点。6.司机点击一键接单时后端使用 Redis 分布式锁防止多人同时抢同一单。7.接单成功后用数据库事务同时更新订单状态和司机状态。8.接单成功后再发布order_taken消息通知其他司机该订单已被抢走。9.司机拒单时记录拒单日志限制每日拒单次数并把订单重新广播给其他司机。10.WebSocket 断开或心跳超时后自动把司机下线避免无效司机继续接收订单。二、主要难点及解决方案1. 订单派发涉及多个服务调用链路长难点在于订单由order-srv创建但司机在线状态、接单、WebSocket 推送都在driver-srv中处理。如果直接让服务之间强耦合调用后续维护会很麻烦。解决方式是使用 RabbitMQ 做异步解耦order-srv只负责创建订单并发布派单消息。driver-srv只负责消费派单消息并推送给司机。两边通过消息队列连接不需要互相强依赖。这样做的好处是下单接口不会因为司机推送失败而阻塞派单逻辑也可以单独扩展。2. 如何让司机实时收到新订单如果只靠司机端轮询待接订单列表会有延迟也会增加服务器压力。派单场景要求实时性比较高所以使用了 WebSocket。解决方式后端维护一个 WebSocket 连接中心Hub。每个在线司机连接后以driver_id作为 key 保存连接。RabbitMQ 收到新订单后调用 WebSocket 广播方法把订单推送给所有在线司机。推送消息类型为new_order。订单被抢后再广播order_taken让其他司机端从列表中移除该订单。涉及实现点包括WSHub.clients保存司机连接。使用sync.RWMutex保证连接 map 并发安全。每个连接单独有mu避免多个 goroutine 同时写同一个 WebSocket 连接。3. 司机在线状态如何保持一致司机在线状态同时存在三处数据库中的service_statusRedis 中的在线状态 hashWebSocket 连接状态这三处很容易出现不一致。例如司机断网了数据库还显示在线或者 Redis key 过期了数据库还没更新。解决方式分成几层司机主动上线时更新数据库service_status 1写入 Redisdriver:online:{driver_id}写入 Redis GEOdriver:geo:set设置 Redis 过期时间司机主动下线时更新数据库service_status 0删除 Redis 在线 hash从 Redis GEO 中移除司机位置WebSocket 断开时自动调用下线逻辑只对service_status 1的司机执行下线如果司机已经是接单中2不强制下线避免影响正在进行的订单Redis 过期兜底后台定时任务扫描数据库中在线司机如果 Redis 在线 key 已不存在则同步把数据库状态改为下线这个设计解决了连接异常、网络断开、Redis 过期、状态残留等问题。4. 多个司机同时抢同一单的并发问题这是接单功能里最核心的难点。如果两个司机几乎同时点击接单只靠查询订单状态再更新很容易出现并发问题两个请求都查到订单是待接单然后都更新成功。解决方式是使用 Redis 分布式锁SET order:lock:{orderNo} lockValue NX EX 5含义是NX只有 key 不存在时才能加锁。EX 5锁 5 秒后自动过期防止死锁。lockValue包含司机 ID 和时间戳用来标识锁持有者。只有抢到锁的司机才能继续执行接单逻辑。没有抢到锁的司机直接返回“订单已被其他司机抢走”。释放锁时用了 Lua 脚本保证只有锁值匹配时才删除锁避免误删其他请求刚拿到的新锁。5. 订单状态和司机状态必须同时成功或同时失败接单成功后需要同时更新两张表订单表设置driver_id订单状态改为1记录接单时间。司机表服务状态改为2表示接单中同时增加总订单数。如果只更新了一张表另一张失败就会出现脏数据。例如订单显示已接单但司机还是空闲状态。解决方式是在模型层封装DoAcceptOrder使用 GORM 事务开启事务。更新订单。更新司机。任意一步失败就回滚。全部成功才提交。这样保证了订单和司机状态的一致性。6. 待接订单列表需要按距离筛选和排序司机端不能看到所有订单而是需要看到附近订单并支持排序、分页、最低价格筛选等。解决方式查询所有order_status 0的待接订单。使用 Haversine 公式计算司机当前位置到订单起点的球面距离。过滤超过搜索半径的订单。支持按距离、价格、时间等维度排序。最后做分页返回。相关知识点是经纬度距离计算CalculateDistance(lat1, lng1, lat2, lng2)这里使用地球半径和三角函数计算两点球面距离比简单的经纬度相减更准确。7. 司机拒单后订单不能消失司机拒单后如果不处理订单可能从当前司机端消失但没有继续推给其他司机。解决方式新增司机拒单日志表driver_reject_log。每次拒单记录司机 ID、订单号、拒单原因、拒单时间。统计当天拒单次数超过上限则限制拒单。拒单后重新把订单发布到order_dispatch队列让其他在线司机继续收到。这样既保证了订单继续流转也能对司机频繁拒单做风控。8. 接单成功后其他司机端的订单要及时移除如果一个订单已经被 A 司机接走B 司机端仍然能看到并点击会造成体验问题。解决方式接单成功后发布order_taken消息。driver-srv消费该消息。WebSocket 广播{ type: order_taken, order_no: xxx }前端收到后从接单大厅列表和地图中移除该订单。这属于典型的“状态变更广播”设计。三、涉及到的核心知识点这次功能涉及的知识点比较多主要包括Go 后端开发包括结构体、方法、错误处理、goroutine、defer、事务封装等。Gin 网关接口司机端接口通过api-gateway暴露例如更新司机在线状态查询待接订单一键接单拒单查询订单详情获取地图订单数据gRPC 和 Protobuf网关通过 gRPC 调用driver-srv、order-srv服务之间用 proto 定义请求和响应结构。GORM 数据库操作包括查询、更新、事务、条件更新、软删除字段过滤、模型方法封装。RedisSetNX实现分布式锁Expire设置在线状态过期时间Hash保存司机在线状态GEO保存司机地理位置Lua 脚本安全释放锁RabbitMQ用消息队列完成异步派单order_dispatch新订单派单广播order_taken订单已被抢通知通过生产者/消费者模式解耦订单服务和司机服务WebSocket用于服务端主动向司机端推送订单解决实时派单问题。并发控制包括Redis 分布式锁WebSocket 连接 map 的读写锁单连接写锁goroutine 后台消费者和心跳检测状态机设计订单状态包括0待接单1已接单2行程中3已完成4已取消司机状态包括0下线1在线2接单中地理位置计算使用 Haversine 公式计算司机和订单起点之间的距离实现附近订单筛选。最终后端实现了从“乘客下单”到“司机实时接单”的完整闭环司机可以上线/下线。在线司机可以实时收到新订单。司机可以查看附近待接订单。多司机抢单时只允许一个成功。接单成功后订单和司机状态同步更新。其他司机会收到订单已被抢通知。司机拒单会记录日志并重新派发。WebSocket 异常断开后司机会自动下线。Redis 状态过期后数据库也能被定时任务修正。整体难点集中在实时性、并发安全、状态一致性和服务解耦最终通过 WebSocket、RabbitMQ、Redis 锁、GORM 事务和状态机校验组合解决。