一、背景:一个看起来简单的需求
最近在做一个同城配送系统的重构,其中一个核心模块是骑手实时位置追踪。
需求很明确:
- 用户可以看到骑手实时位置
- 能计算距离
- 预计到达时间动态更新
整体流程看起来并不复杂:
骑手 App → 后端 → 用户小程序但真正上线之后才发现,位置服务远比想象复杂,问题主要集中在三类:
- GPS不稳定
- 坐标系不统一
- 逆地址解析成本过高
这篇文章做一个完整复盘。
二、系统整体架构设计
整体链路如下
骑手端(Android) ↓ 每15秒上报 Node.js后端 ↓ 轨迹存储 / 坐标转换 / 地址解析 Redis / DB ↓ WebSocket推送 ↓ 用户端(小程序 / H5)位置能力拆解后主要包括:
- GPS定位
- WiFi/基站融合定位
- 坐标转换(WGS84 / GCJ02)
- 正逆地址解析
- 轨迹存储与查询
三、问题一:GPS失效导致位置“卡住不动”
1. 现象
用户反馈:
- 骑手明明已经在楼里
- 但地图位置长时间不更新
- 看起来像“停在原地”
2. 原因分析
GPS在以下场景容易失效:
- 商场内部
- 地下停车场
- 电梯 / 写字楼内部
- 高楼密集区域
如果只依赖GPS:
GPS失败 → 不上报 → 用户看到旧位置3. 优化方案:融合定位 + 降级策略
改造为三级定位策略:
GPS(优先) ↓ WiFi + 基站融合定位 ↓ 失败则不上报状态Android 示例:
suspend fun getAndReportLocation() { val gps = awaitGps(timeoutMs = 3000) if (gps != null && gps.accuracy <= 30f) { upload(gps.lat, gps.lng, "gps") return } val wifiList = scanWifi() val cellList = scanCells() val fused = getFusedLocation(wifiList, cellList) if (fused != null) { upload(fused.lat, fused.lng, "fused") return } reportFailed() }4. 关键经验
- 不要用
0,0表示失败 - 要明确记录定位失败状态
- 室内场景必须依赖融合定位
四、问题二:轨迹跳点 / 穿楼问题
1. 现象
用户看到轨迹出现:
- 瞬移
- 穿楼
- 穿河
- 折线异常跳动
2. 根本原因:坐标系混用
系统中混用了三类数据:
| 来源 | 坐标系 |
|---|---|
| GPS | WGS84 |
| 第三方地图 | GCJ02 |
| 历史数据 | BD09 |
问题在于:
数据存储时没有记录坐标系字段
3. 数据结构优化
新增字段:
coord_system TINYINT NOT NULL COMMENT '0=WGS84 1=BD09 3=GCJ02'4. 统一策略:入库统一 GCJ02
最终方案:
所有来源 → 坐标转换 → GCJ02 → 入库后端强制校验:
if (coordSystem == null) { throw new IllegalArgumentException("坐标系不能为空"); }5. 历史数据迁移
批量转换逻辑:
async function convertBatch(records, from) { const res = await fetch('/geoconv', { method: 'POST', body: JSON.stringify({ from, points: records.map(r => ({ lat: r.lat, lng: r.lng })) }) }).then(r => r.json()) return res.list }6. 关键经验
- 坐标系一定要“强约束”
- 不允许混存
- 渲染层不要做转换
五、问题三:逆地址解析调用量爆炸
1. 现象
上线后发现:
- 位置服务费用上涨明显
- 调用量远超预期
2. 原因
最初设计:
用户请求位置 → 逆地址解析 → 返回地址问题在于:
- 骑手15秒上报一次
- 用户5秒刷新一次
- 同一位置被重复解析
3. 优化方案:写路径解析
改造后:
骑手上报 → 逆地址解析 → 存数据库 用户读取 → 直接返回4. 示例代码
async function handleUpload({ riderId, lat, lng }) { const geo = await reverseGeocode(lat, lng) await db.save({ riderId, lat, lng, address: geo?.name }) pushToUser(riderId, { lat, lng, address: geo?.name }) }5. 可选优化:GeoHash缓存
进一步优化:
GeoHash → Redis → 地址缓存减少重复逆解析请求。
六、最终架构总结
最终位置链路如下:
骑手App ↓ GPS / 融合定位 ↓ 坐标统一转换(GCJ02) ↓ 逆地址解析(写路径) ↓ 轨迹存储 ↓ WebSocket推送 ↓ 用户展示七、核心经验总结
1. 坐标系必须统一
- 入库统一 GCJ02
- 不允许混存
2. 定位必须有降级策略
GPS → 融合定位 → 失败状态
3. 逆解析必须走写路径
不要在读请求中做计算
4. 失败状态要显式记录
不能用伪坐标替代
八、写在最后
实时位置系统的难点不在“地图展示”,而在:
- 数据一致性
- 坐标体系
- 弱信号场景
- 成本控制
这些问题在开发阶段不明显,但在真实业务中会被迅速放大。
如果一开始就把位置能力抽象成统一服务层,后续维护成本会低很多。
本案例示例采用迈云位置服务,感兴趣或者有需要的 可以看看
迈云位置服务 LTS - 高精度位置 API 与 SDK