我用一套 API 重构了公司的位置能力:从三套地图 SDK 到统一位置中台的实战

0 阅读5分钟

去年我们做了一件很典型的“系统债务清理”:把三个业务线里分散的位置能力,统一收敛成一个位置中台。

表面看是“统一 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 坐标
  • 标准地址

本质是多源数据加权估计。


四、位置中台设计

统一位置架构图(逻辑视图)

image.png

对外统一封装:

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”,而是:

把位置能力从业务实现层,抽象成统一语义层

当这一层成立之后:

  • 数据不再分裂
  • 业务不再依赖厂商
  • 系统才真正可演进