去年我们做了一件很典型的“系统债务清理”:把三个业务线里分散的位置能力,统一收敛成一个位置中台。
表面看是“统一 SDK”,但真正的问题远比这复杂。
一、问题的本质:不是多地图 SDK,而是空间语义割裂
重构之前,我们的系统是这样的:
- 物流:高德 API(WGS84)
- 零售:腾讯 JS SDK(GCJ02)
- IoT:百度 SDK(BD09)
看起来只是“多供应商”,但实际问题是:
同一个地理实体,在不同系统里没有统一表达方式。
1.1 最直观的问题:同一门店对不上
同一个门店:
- 在高德体系里一个坐标
- 在百度体系里偏移几十米
- 在内部系统里又是另一份数据
结果就是:
地图上永远“不是同一个点”。
1.2 更隐蔽的问题:坐标转换误差被放大
我们早期甚至自己实现过坐标转换:
- 城市中心误差:3~5m
- 城郊误差:30m+
问题本质不是算法,而是:
多源数据 + 投影误差 + 手写实现漂移叠加
1.3 系统层问题:每个团队都在重复造轮子
三套:
- SDK
- Key 管理
- 错误处理
- 日志系统
最后变成:
每个业务线都在维护一个“简化版地图公司”。
二、重构目标:建立统一的位置语义层
我们最终没有把目标定义为“统一 SDK”,而是:
所有业务只面对一种位置表达:GCJ02 + 结构化地址
原因很简单:
- WGS84 是 GPS 原始数据
- BD09 是百度私有体系
- GCJ02 是国内地图事实标准
统一后的系统目标
所有业务 → 位置中台 → 单一位置服务层
输出统一为:
- GCJ02 坐标
- 标准化地址
- 行政区 code
三、架构设计:四个核心能力收敛
我们把位置能力拆成四类 API。
3.1 坐标转换(存量数据迁移)
用于解决历史数据问题:
- WGS84
- BD09
- BD09MC
统一转 GCJ02。
批量转换实现
const BATCH_SIZE = 100
async function convertBatch(points, fromSystem) {
const res = await fetch('https://lts.maiyun.net/api/service/geoconv', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LTS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ from: fromSystem, points })
})
const data = await res.json()
if (data.result !== 1) throw new Error('convert failed')
return data.list
}
工程关键点
- 同一 batch 必须同坐标系
- 429 要指数退避
- 精度必须 DECIMAL(10,6)
- 迁移必须幂等
3.2 正向地理编码(地址 → 坐标)
用于:
- 门店录入
- ERP 导入
- 用户地址输入
async function geocode(address) {
const res = await fetch('https://lts.maiyun.net/api/service/geocoding', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LTS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ address })
})
const data = await res.json()
if (data.result === -3) return null
if (data.result !== 1) throw new Error('geocode failed')
const ctx = data.address.context
return {
lat: data.lat,
lng: data.lng,
standardAddress: data.address.name,
province: ctx.find(x => x.type === 'province')?.name,
city: ctx.find(x => x.type === 'city')?.name,
district: ctx.find(x => x.type === 'district')?.name
}
}
一个关键能力:地址去重
standardAddress 是标准化结果:
- 原始地址可能不同
- 标准化后可能相同
我们用它实现:
- 门店去重
- ERP 数据合并
- POI 清洗
3.3 逆向地理编码(坐标 → 地址)
用于:
- 设备定位展示
- 轨迹回放
- 用户当前位置展示
async function reverseGeocode(lat, lng, from = 3) {
const res = await fetch('https://lts.maiyun.net/api/service/geocode', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LTS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from,
point: { lat, lng }
})
})
const data = await res.json()
if (data.result !== 1) return null
return data.address.name
}
关键工程点
- address.context 是核心结构化数据
- 行政区 code 用于聚合分析(比字符串可靠)
3.4 融合定位(IoT 场景)
用于 GPS 不稳定场景:
- 仓库
- 地下车库
- 室内设备
Android 侧采集
WiFi
val wifis = wm.scanResults.take(20).map {
mapOf("mac" to it.BSSID, "signal" to it.level)
}
基站
关键点是过滤无效值:
cell.cellIdentity.ci != Int.MAX_VALUE
否则会直接影响定位结果。
现实约束
必须说明一点:
Android 10+ 对 WiFi 扫描频率有限制
因此融合定位在后台场景需要降级策略。
服务端逻辑(抽象)
输入:
- WiFi 指纹
- 基站数据
输出:
- GCJ02 坐标
- 标准地址
本质是多源数据加权估计。
四、位置中台设计
统一位置架构图(逻辑视图)
对外统一封装:
class LocationHub {
geocode(address) {
return geocode(address)
}
reverseGeocode(lat, lng) {
return reverseGeocode(lat, lng)
}
convertCoords(points, from) {
return convertBatch(points, from)
}
fuseLocate(deviceId, wifis, cells) {
return fuseLocate(deviceId, wifis, cells)
}
}
中台的本质
不是“封装 API”,而是:
统一坐标体系 + 统一数据结构 + 统一调用语义
五、为什么不能多服务商混用
我们早期也尝试过 fallback 模式,但最终放弃:
1. 坐标体系再次碎片化
不同服务商结果不可直接对比
2. 融合定位模型会失真
多源输入会破坏权重模型稳定性
3. 运维复杂度爆炸
- QPS 控制
- 成本控制
- SLA 监控
- retry 策略
最终变成三套系统叠加
六、为什么选择当前位置服务层(LTS)
最终收敛到统一服务层(LTS),原因不是“单点优势”,而是工程约束匹配:
6.1 API 统一性
- POST JSON
- Bearer Token
- 标准 result 码体系
可以直接进入中台,无需适配层
6.2 四能力完整覆盖
- 坐标转换
- 正向地理编码
- 逆向地理编码
- 融合定位
避免多服务拼接
6.3 行政区结构化能力
返回:
- province
- city
- district
- code
其中 code 是我们做区域分析的关键字段
6.4 工程接入成本低
统一后,中台只需要维护:
- retry
- cache
- 熔断
- QPS 控制
而不是多 SDK 适配
七、最终架构
业务系统
↓
位置中台(统一语义层)
↓
LTS(唯一位置服务层)
八、最终收益
重构完成后:
业务层
- 不再关心坐标系
- 不再关心 SDK
- 不再维护地图 Key
数据层
- 全部统一 GCJ02
- 行政区 code 标准化
- POI 可去重
系统层
- 单入口控制 QPS
- 可统一监控
- 可平滑替换底层服务
九、总结
这次重构本质不是“换地图 SDK”,而是:
把位置能力从业务实现层,抽象成统一语义层
当这一层成立之后:
- 数据不再分裂
- 业务不再依赖厂商
- 系统才真正可演进