问题
一个通过应用接受预约的出租车服务,需要的远不止一个预约表单。它必须找到附近空闲的司机、向其派单、等待回应,如果没人接单就转给下一位,而且要在几秒钟内完成。罗马还有自己的规则:往返机场和港口的行程实行市政府核定的固定价格。客户需要的是从调度到支付的完整链条。
我做了什么
我开发了一个实时预约与调度平台,包括一个后端、三个移动应用和一个 Web 管理后台。
- 调度引擎——它在搜索半径内(默认 8 km)寻找已审核、可接单且在过去 60 秒内上报过位置的司机。它根据 Google 提供的当前路况下的实际行车时间,对最多 10 位司机进行排序,然后逐一向司机派单,每位司机有 20 秒时间决定是否接单。
- 罗马计价规则——计价服务可识别 9 个关键地点,例如菲乌米奇诺机场和历史中心区,并执行 Roma Capitale 规定的 12 项固定价格(第 252/2024 号决议)。其他行程则按 Google 路线计价,另加行李和乘客附加费。
- 三个应用——乘客应用、司机应用和车载平板应用,平板应用显示路线和合作伙伴横幅。全部使用 React Native 和 Expo 开发,支持 Android 和 Web。
- 实时追踪与通话——Socket.IO 推送司机位置和行程状态。乘客和司机可以通过 WebRTC 在应用内互相通话,而不会看到对方的电话号码。
- 支付——Stripe(webhook 经过签名校验)和 PayPal 订单。支付密钥在管理面板中设置,并加密保存。
- 司机入驻——司机上传驾照、证件和照片,经管理员审核通过后才能上线接单。
- 赞助商与优惠券——设有三种套餐的赞助商门户和带防欺诈追踪的优惠券,另外还支持预约行程和评价。
技术栈
后端运行在 Node.js 22 上,使用 TypeScript、Express 和 Socket.IO。数据存储在 MariaDB/MySQL 中,数据库结构历经 24 次版本化的 SQL 迁移逐步完善。Zod 校验输入;登录使用 JWT 和 Google 登录。Google Maps 负责路线、行程时间和地理编码。应用使用 Expo 和 React Native。
为什么这样做
两位乘客绝不能分到同一位司机。司机接单时,引擎用一条条件更新语句锁定该司机:设为忙碌,但前提是他仍处于空闲状态。如果没有任何记录被修改,说明这位司机刚刚被别人占用,行程就转给下一位候选人。候选司机按行车时间而不是直线距离排序,因为在罗马,最近的车往往不是最快的。如果 Google 没有响应,排序就退回按距离进行。


