小程序定位为啥偏了几百米?WGS84 和 GCJ02 坐标系混用是主因,附转换踩坑

发布时间:2026/10/10 13:49:59
小程序定位为啥偏了几百米?WGS84 和 GCJ02 坐标系混用是主因,附转换踩坑
被定位问题折磨过的人都懂App 上点好好的一上小程序/H5marker 就偏出去两三百米。一开始以为 GPS 漂移后来发现根本不是——是坐标系没对上。微信小程序uni.getLocation默认返回 WGS84 原始坐标而高德、腾讯地图的 JS SDK 用的是 GCJ02火星坐标两者在国内差着几百米。这个坑我踩完写了个coordTransform.js放招聘小程序里今天把转换逻辑和踩坑过程摊开讲。为什么会有两种坐标WGS84 是 GPS 卫星原始坐标全球通用。国内出于安全考虑公开发行的地图服务商会把坐标做一次非线性偏移得到 GCJ02俗称火星坐标。百度更狠在 GCJ02 基础上再偏移一次得到 BD09。结果就是同一个位置WGS84、GCJ02、BD09 三个数值全不一样。你在 H5 上用浏览器定位拿到 WGS84直接拿去高德地图画 marker不偏才怪。转换工具WGS84 转 GCJ02我在utils/coordTransform.js里实现了转换H5 定位结果统一转成业务侧标准坐标constPIMath.PI;constAXIS6378245.0;// 长半轴constEE0.00669342162296594323;// 偏心率平方functionoutOfChina(lng,lat){returnlng72.004||lng137.8347||lat0.8293||lat55.8271;}/** WGS84 → GCJ02入参/出参均为 { lng, lat } */exportfunctionwgs84ToGcj02(lng,lat){constxNumber(lng);constyNumber(lat);if(!isFinite(x)||!isFinite(y)){return{lng:x,lat:y};}if(outOfChina(x,y)){return{lng:x,lat:y};// 境外不做偏移}letdLattransformLat(x-105.0,y-35.0);letdLngtransformLng(x-105.0,y-35.0);constradLat(y/180.0)*PI;letmagicMath.sin(radLat);magic1-EE*magic*magic;constsqrtMagicMath.sqrt(magic);dLat(dLat*180.0)/(((AXIS*(1-EE))/(magic*sqrtMagic))*PI);dLng(dLng*180.0)/((AXIS/sqrtMagic)*Math.cos(radLat)*PI);return{lng:xdLng,lat:ydLat};}transformLat和transformLng是那组著名的多项式偏移公式直接抄标准实现就行别自己发明偏移系数差一点结果就差几十米functiontransformLat(lng,lat){letret-100.02.0*lng3.0*lat0.2*lat*lat0.1*lng*lat0.2*Math.sqrt(Math.abs(lng));ret((20.0*Math.sin(6.0*lng*PI)20.0*Math.sin(2.0*lng*PI))*2.0)/3.0;ret((20.0*Math.sin(lat*PI)40.0*Math.sin((lat/3.0)*PI))*2.0)/3.0;ret((160.0*Math.sin((lat/12.0)*PI)320*Math.sin((lat*PI)/30.0))*2.0)/3.0;returnret;}业务侧的封装定位完直接归一H5 端uni.getLocation拿到的就是 WGS84我封装了一个归一函数定位回调里直接调用后续代码永远只处理 GCJ02/** 将 uni.getLocation(wgs84) 结果转为 GCJ02 经纬度对象 */exportfunctionnormalizeH5LocationToGcj02(res){constlngNumber(resres.longitude);constlatNumber(resres.latitude);constgcjwgs84ToGcj02(lng,lat);return{...res,latitude:gcj.lat,longitude:gcj.lng,};}调用方就干净了uni.getLocation({type:wgs84,// 明确要原始坐标success(res){constlocnormalizeH5LocationToGcj02(res);this.lngloc.longitude;this.latloc.latitude;// 后续传给后端、画地图全用 GCJ02}});注意type: wgs84必须显式指定uni.getLocation在部分端默认返回的类型不一样不写死容易踩今天对明天错的坑。反过来的情况后端存的到底是哪种坐标有转换就有反转换GCJ02 转回 WGS84 的公式是对偏移做迭代逼近网上有标准实现但我建议能不用就不用。我们的约定是后端存储和对外输出统一 GCJ02前端所有定位入口进来就归一反转换只在特殊场景比如要跟 GPS 硬件原始数据对拍才用。百度的 BD09 是另一个坑。如果哪天地图服务切到百度记得百度返回的坐标是 BD09得再做一步转成 GCJ02 再存库不然下一次切回高德库里全是百度坐标历史数据全废/** BD09 → GCJ02百度地图坐标转业务标准坐标截取关键偏移部分 */exportfunctionbd09ToGcj02(lng,lat){constxlng-0.0065;constylat-0.006;constzMath.sqrt(x*xy*y)-0.00002*Math.sin(y*PI);constthetaMath.atan2(y,x)-0.000003*Math.cos(x*PI);return{lng:z*Math.cos(theta),lat:z*Math.sin(theta)};}这套约定写进项目文档后前后端再没因为坐标吵过架前端管转换后端只管存谁也不要试图在另一端纠正。定位失败和权限拒绝也得处理坐标转换写得再对定位失败一切白搭。uni.getLocation失败最常见三种用户拒绝授权、系统定位没开、H5 在非 HTTPS 环境拿不到定位。我们的兜底策略uni.getLocation({type:wgs84,success(res){constlocnormalizeH5LocationToGcj02(res);this.lngloc.longitude;this.latloc.latitude;},fail(err){// 降级取用户手动选择的城市中心坐标constcitythis.cityCenter||{lng:114.9312,lat:25.6610};this.lngcity.lng;this.latcity.lat;this.locationFallbacktrue;// 标记降级UI 提示定位失败已使用默认位置}});降级坐标别用全局写死的常量应该取用户上次成功定位或手动选择的城市中心不然南昌的用户失败一次就掉到赣州去了。locationFallback标记很关键前端列表页要展示位置可能不准的提示避免用户按错误距离排序骂娘。招聘场景还有个特殊需求企业发布职位填地址选完省市区要逆地理编码出经纬度或者用户搜南康家具城要 POI 检索。这两条链路同样统一走 GCJ02前端把选中的坐标原样传后端后端存库时不转换、不偏移保持与地图服务商一致——存进去和拿出来是同一个坐标系是最低要求。踩坑记录200 米偏差排查了两天现象招聘小程序附近职位里H5 端用户点进详情地图 marker 跟实际位置差 200 多米微信端却正常。排查过程先怀疑后端存错坐标把库里的经纬度拉出来跟真实地址对没错再怀疑高德 SDK 配置清缓存、换 key 都不行最后把定位原始值打出来uni.getLocation返回{lng: 114.9312, lat: 25.6610}但高德地图直接画的点是114.9289, 25.6591——两个值差着 0.002 个经度。定位思路微信端getLocation默认就是 GCJ02type 缺省H5 端浏览器定位给的是 WGS84同一份代码两套坐标系。之前只在微信测过没暴露一上 H5 就翻车。最终解决所有端统一type: wgs84拿原始坐标进normalizeH5LocationToGcj02转成 GCJ02 再往下走从此前后端只认一种坐标。另外outOfChina这个判断别删境外坐标做偏移会越偏越离谱海南、新疆这些边缘地区的坐标也要靠它兜底。可以直接抄走的清单端上坐标不一致微信getLocation缺省 GCJ02H5 是 WGS84必须显式type: wgs84。标准公式直接抄transformLat/transformLng用公开标准实现不自己发明系数。境外兜底outOfChina范围外不做偏移否则越偏越远。数值校验isFinite拦住空值/异常值别让NaN污染下游。前后端只认一种业务侧统一 GCJ02各端在入口归一后端永远不碰坐标系。项目源码https://gitee.com/gzqkl/job-uniapp