简介:这是一份面向Java后端开发者与小程序入门者的实战型项目源码,围绕「小程序地图定位」这一常见移动场景,演示如何用Java技术栈配合前端完成位置服务。资源共38个文件,以15张png界面截图与图标、6个js逻辑脚本、5个wxss样式、4个wxml页面结构及4个json配置为主,另含说明文档与开源协议,压缩包约314KB,体量轻便,便于快速导入与阅读。内容覆盖GPS与网络定位、地理编码与反地理编码、路径规划、位置实时更新、隐私安全处理及前后端接口设计等关键环节,并涉及高德、百度等地图SDK的集成思路。目录按pages、utils、image等模块划分,结构清晰,适合对照学习小程序页面组织与后端交互方式。目前已有155人学习下载,可作为课程设计、练手项目或地图定位功能开发的参考范例。
1. 基于 Java 开发的小程序地图定位:从后端签名到前端选点的完整链路
用户在小程序里点一下「获取我的位置」,地图上立刻出现一个蓝点,周边门店按距离排好序——这个体验背后其实横跨了三层:小程序端的wx.getLocation与map组件、微信服务端对key的校验、以及 Java 后端对坐标的存储与逆地理编码。很多人第一次做「基于 Java 开发的小程序地图定位」,卡住的地方往往不是前端画不出地图,而是后端拿到的经纬度对不上、签名报INVALID_USER_SCODE、或者真机上定位直接超时。这篇笔记按我实际落地的顺序拆:先讲清坐标系和权限这两个绕不开的前提,再给出一套 Java 后端 + 小程序前端能跑通的最小实现,最后把几个血泪踩坑点摊开讲。适合已经会写 Spring Boot 接口、但没系统做过地图定位的小程序开发者,也适合想搞清楚「定位数据到底该存哪、怎么存」的后端同学。
2. 坐标系、权限与选型:动手前必须定下来的三件事
2.1 坐标系不统一,是定位偏移的第一个黑匣子
国内做地图定位,绕不开三套坐标系:WGS84 是 GPS 原始坐标,GCJ02 是国测局加密后的坐标(腾讯地图、高德地图用的就是这套),BD09 是百度在 GCJ02 上又加了一层偏移。小程序里wx.getLocation的type参数默认是wgs84,但如果你要在地图上打点、或者调腾讯位置服务的逆地理编码接口,就必须传gcj02,否则会出现几十到几百米的系统性偏移。
我一般会这样定规矩:前端拿坐标一律用gcj02,后端存储也统一存gcj02,只在需要和 GPS 设备原始数据对接时才做转换。转换本身有公开的数学公式,但更省事的做法是直接调腾讯位置服务的坐标转换接口,避免自己实现时精度对不上。这里的关键是「统一」,最怕的是前端传 gcj02、后端某张老表里存的是 wgs84,两个数据一 join 距离全乱。
2.2 小程序定位权限:不是申请了就一定有
wx.getLocation从基础库某个版本起,需要在app.json里声明requiredPrivateInfos,否则真机上直接失败。同时用户侧还有两层授权:小程序级别的「位置信息」授权,以及手机系统级别的定位开关。很多新手在开发者工具里跑得好好的,一到真机就报getLocation:fail auth deny,八成是漏了声明或者用户拒过一次后没引导重新授权。
{ "requiredPrivateInfos": ["getLocation", "chooseLocation"], "permission": { "scope.userLocation": { "desc": "用于展示您附近的门店并计算距离" } } }这段配置写在app.json里。requiredPrivateInfos是硬性声明,缺了接口直接不可用;permission.scope.userLocation.desc是授权弹窗里给用户看的说明文案,写清楚用途能明显提高授权率。注意chooseLocation也要一起声明,否则用户手动选点时同样会失败。
2.3 Java 后端选型:为什么我倾向腾讯位置服务而不是自己搭
后端要做的事其实就两件:把坐标存下来、把坐标翻译成地址(逆地理编码)。逆地理编码自己搭不现实,必须用第三方。国内主流是腾讯位置服务和高德,选腾讯的理由很直接——小程序生态本身就是腾讯的,wx.getLocation拿到的 gcj02 坐标可以直接喂给腾讯的 WebService API,不用再做坐标系转换,少一层出错的可能。
Java 侧调用就是普通的 HTTP 请求,用RestTemplate或OkHttp都行,不需要引入什么重型 SDK。真正要设计的是服务端签名:腾讯位置服务的 WebService API 支持 SK 签名校验,签名串是「请求路径 + 参数排序拼接 + SK」做 MD5。这个签名必须在后端做,绝不能把 SK 放到小程序里,否则等于把密钥公开了。
| 方案 | 坐标系 | 签名位置 | 适合场景 |
|---|---|---|---|
| 腾讯位置服务 | GCJ02 | Java 后端 | 小程序原生定位、门店距离排序 |
| 高德 Web 服务 | GCJ02 | Java 后端 | 已有高德生态、需要路径规划 |
| 自建 GeoHash 索引 | 任意 | 无 | 只做附近检索、不做地址解析 |
选型定下来之后,后面所有代码都围绕「前端传 gcj02、后端签名调腾讯、结果落库」这条主线走。
3. Java 后端:签名、逆地理编码与附近检索的落地代码
3.1 服务端签名:SK 绝不能出现在小程序里
腾讯位置服务的签名规则是:把请求路径和所有参数按 key 字典序排列,拼成path?key1=value1&key2=value2的形式,末尾直接拼上 SK,然后做 MD5,得到的结果就是sig参数。注意参数值要用原始值,不要先做 URL 编码。
import java.security.MessageDigest; import java.util.Map; import java.util.TreeMap; public class TencentMapSign { /** * 生成腾讯位置服务 WebService API 签名 * @param path 接口路径,如 /ws/geocoder/v1/ * @param params 业务参数(不含 sig) * @param sk 服务端密钥 */ public static String sign(String path, Map<String, String> params, String sk) { // TreeMap 保证 key 按字典序排列 TreeMap<String, String> sorted = new TreeMap<>(params); StringBuilder sb = new StringBuilder(path).append("?"); for (Map.Entry<String, String> e : sorted.entrySet()) { sb.append(e.getKey()).append("=").append(e.getValue()).append("&"); } // 末尾拼 SK,注意这里没有额外的 & 分隔 sb.append(sk); return md5(sb.toString()); } private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes("UTF-8")); StringBuilder hex = new StringBuilder(); for (byte b : digest) { hex.append(String.format("%02x", b)); } return hex.toString(); } catch (Exception e) { throw new RuntimeException("签名计算失败", e); } } }逻辑说明:TreeMap天然按 key 排序,省去手动排序;拼接时每个参数后面都带&,最后直接接 SK,这是腾讯文档里明确的格式,多一个或少一个&都会导致签名不匹配。参数说明:path必须和实际请求的路径完全一致(包括结尾的斜杠),params里不要放sig本身,sk从配置中心或环境变量读取,不要硬编码在代码里。
3.2 逆地理编码接口:把经纬度翻译成「XX 路 XX 号」
拿到签名后,调/ws/geocoder/v1/接口,传location=lat,lng(注意腾讯的格式是纬度在前、经度在后,这点和很多人的直觉相反)。
import org.springframework.web.client.RestTemplate; import org.springframework.http.ResponseEntity; import java.util.HashMap; import java.util.Map; public class GeocoderService { private static final String PATH = "/ws/geocoder/v1/"; private final RestTemplate restTemplate = new RestTemplate(); private final String key; private final String sk; public GeocoderService(String key, String sk) { this.key = key; this.sk = sk; } public String reverseGeocode(double lat, double lng) { Map<String, String> params = new HashMap<>(); params.put("key", key); params.put("location", lat + "," + lng); // 纬度在前 String sig = TencentMapSign.sign(PATH, params, sk); params.put("sig", sig); StringBuilder url = new StringBuilder("https://apis.map.qq.com").append(PATH).append("?"); params.forEach((k, v) -> url.append(k).append("=").append(v).append("&")); ResponseEntity<String> resp = restTemplate.getForEntity(url.toString(), String.class); // 实际项目里应解析 JSON,取 result.address 字段 return resp.getBody(); } }逻辑说明:先算签名再拼 URL,顺序不能反;location的纬度在前是腾讯的约定,传反了会返回「参数错误」或定位到地球另一端。参数说明:key是小程序绑定的 key,sk是配套的服务端密钥,两者在腾讯位置服务控制台都能拿到。返回的 JSON 里result.address是结构化地址,result.formatted_addresses.recommend是更适合展示的推荐地址。
3.3 附近检索:用数据库算距离还是用 GeoHash
门店距离排序有两种常见做法。数据量小(几千条以内)时,直接存经纬度,用 SQL 的球面距离公式算,简单直接。数据量大或者 QPS 高时,上 GeoHash 或者 Redis 的 GEO 结构。
-- 球面距离近似计算,单位米,适用于小数据量 SELECT id, name, 6371000 * 2 * ASIN(SQRT( POWER(SIN((? - latitude) * PI() / 360), 2) + COS(? * PI() / 180) * COS(latitude * PI() / 180) * POWER(SIN((? - longitude) * PI() / 360), 2) )) AS distance FROM shop HAVING distance < 3000 ORDER BY distance ASC LIMIT 20;逻辑说明:这是 Haversine 公式的 SQL 实现,6371000是地球半径(米),三个?分别是用户纬度、用户纬度、用户经度。参数说明:HAVING distance < 3000限定 3 公里内,LIMIT 20控制返回条数。注意这个写法无法走索引,全表扫描,数据量上万后要换成「先用矩形范围过滤、再精算距离」的两段式查询,或者直接上 Redis GEO。
提示:经纬度字段建议用
DECIMAL(10,7)存储,FLOAT在距离计算时精度不够,容易出现「明明很近却排到后面」的玄学问题。
4. 小程序前端:getLocation、map 组件与选点的配合
4.1 getLocation 的正确调用姿势
前端拿定位的核心就一个接口,但参数和失败处理要做全。
wx.getLocation({ type: 'gcj02', // 必须和地图、后端统一 isHighAccuracy: true, // 开启高精度,室内定位更准 highAccuracyExpireTime: 4000, success(res) { const { latitude, longitude } = res; // 传给后端做逆地理编码和附近检索 wx.request({ url: 'https://your-domain.com/api/nearby', data: { lat: latitude, lng: longitude } }); }, fail(err) { // 区分「用户拒绝」和「系统定位关闭」 if (err.errMsg.includes('auth deny')) { wx.showModal({ title: '需要位置权限', content: '请在设置中开启位置信息授权', success(r) { if (r.confirm) wx.openSetting(); } }); } else { wx.showToast({ title: '定位失败,请检查系统定位开关', icon: 'none' }); } } });逻辑说明:type: 'gcj02'是整条链路统一坐标系的第一步;isHighAccuracy开启后会尝试 GPS + 基站 + WiFi 混合定位,室内场景明显更准,但耗时略长,所以配了highAccuracyExpireTime做超时兜底。参数说明:highAccuracyExpireTime单位毫秒,超过这个时间还没拿到高精度结果就返回当前最优结果,避免一直转圈。失败回调里区分auth deny和其他错误很重要,前者要引导去设置页,后者多半是系统定位没开。
4.2 map 组件打点与 chooseLocation 手动选点
拿到坐标后,用map组件展示,markers数组里放标记点,latitude/longitude控制中心点。
Page({ data: { latitude: 39.908, longitude: 116.397, markers: [] }, onLoad() { this.locate(); }, locate() { wx.getLocation({ type: 'gcj02', success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude, markers: [{ id: 1, latitude: res.latitude, longitude: res.longitude, width: 24, height: 24, callout: { content: '我的位置', display: 'ALWAYS' } }] }); } }); }, choosePoint() { wx.chooseLocation({ success: (res) => { // res 里同样带 latitude/longitude,坐标系也是 gcj02 this.setData({ latitude: res.latitude, longitude: res.longitude, markers: [{ id: 2, latitude: res.latitude, longitude: res.longitude }] }); } }); } });逻辑说明:wx.chooseLocation打开的是腾讯地图选点页,返回的坐标同样是 gcj02,可以直接和getLocation的结果混用,不用转换。参数说明:markers里每个点的id要唯一,callout的display: 'ALWAYS'让气泡常显,适合展示「我的位置」这类固定标记。注意map组件是原生组件,层级最高,弹窗类 UI 要避开它,否则会被盖住。
4.3 前后端联调时最容易对不上的两个点
第一个是坐标系:前端传 gcj02,后端如果拿去做 WGS84 的逆地理编码,地址会偏。第二个是经纬度顺序:小程序里latitude在前,腾讯 WebService 也是纬度在前,但很多第三方库(比如某些 GeoHash 实现)习惯经度在前,中间层转换时容易传反。我一般会在接口文档里明确写「lat,lng 顺序」,并在后端加一个范围校验——纬度绝对值不超过 90、经度不超过 180,传反了大概率触发校验失败,比默默算错强。
5. 避坑与排查:定位偏移、签名失败、真机超时的处理清单
5.1 现象:地图上蓝点和实际位置差几百米
原因:坐标系混用。最常见的是前端用了默认的wgs84,后端或地图按 gcj02 处理。解决:全局搜索getLocation调用,确认type都是gcj02;检查数据库里历史数据的坐标系标注,必要时写脚本批量转换。
5.2 现象:接口返回INVALID_USER_SCODE或签名错误
原因:签名串拼接格式不对,或者参数里混入了sig本身、或者参数值做了 URL 编码后再签名。解决:打印出签名前的原始字符串,逐字符比对腾讯文档的示例;确认path和实际请求路径完全一致;确认 SK 没有多余空格。
5.3 现象:开发者工具正常,真机getLocation:fail
原因:app.json漏了requiredPrivateInfos,或者用户之前拒绝过授权且没有重新引导。解决:补声明;在fail回调里判断auth deny并调wx.openSetting;测试时可以在手机设置里先清掉小程序的授权记录再试。
5.4 现象:室内定位漂移到隔壁楼
原因:纯 GPS 在室内信号弱,isHighAccuracy没开或者超时太短。解决:开启isHighAccuracy,把highAccuracyExpireTime调到 4000 以上;对精度要求高的场景,可以结合 WiFi 指纹或让用户手动chooseLocation校正。
5.5 现象:附近门店排序结果不稳定
原因:距离计算用了FLOAT精度不够,或者 SQL 里HAVING和ORDER BY的字段不一致。解决:经纬度改DECIMAL(10,7);确认ORDER BY用的是计算出的distance别名;数据量大时改用两段式查询避免全表扫描。
6. 进阶:把定位精度和检索性能再往上提一档
基础链路跑通后,真正拉开差距的是两件事:定位精度和检索性能。定位精度上,wx.getLocation的isHighAccuracy只是第一步,如果业务对室内定位要求高,可以在拿到粗略坐标后,让用户手动微调,或者接入腾讯的定位 SDK 做 WiFi 辅助定位。我一般会在「自动定位 + 手动校正」之间做个平衡:自动定位给个初始点,地图上允许用户拖动标记点,拖动结束后反查地址,这样既省事又准。
检索性能上,前面说的 Haversine 全表扫描在数据量上万后就会明显变慢。我的做法是先用一个矩形范围把候选集缩小,再精算距离:
-- 第一段:矩形范围粗筛,能走索引 SELECT id, name, latitude, longitude FROM shop WHERE latitude BETWEEN ? - 0.027 AND ? + 0.027 AND longitude BETWEEN ? - 0.035 AND ? + 0.035; -- 第二段:在 Java 里对粗筛结果做 Haversine 精算并排序逻辑说明:纬度 1 度约 111 公里,0.027 度大约是 3 公里;经度要乘以纬度的余弦值,北京纬度约 40 度,余弦约 0.77,所以 3 公里对应约 0.035 度。参数说明:这两个偏移量要根据业务覆盖范围调整,范围越大粗筛越松、精算越多。这个两段式写法能让latitude上的索引生效,实测比全表扫描快一个数量级。
再往上就是 Redis GEO,把门店坐标写进GEOADD,用GEORADIUS直接查附近,性能最好,代价是多维护一份数据、要处理缓存和数据库的一致性。我的习惯是:日活不高、门店几千条以内,SQL 两段式足够;门店上万或者要做实时附近的人,再上 Redis GEO。
最后说个验证方法:拿几个已知坐标的点(比如公司、家、常去的商场),分别在开发者工具和真机上跑一遍,对比返回的地址和距离。如果开发者工具准、真机偏,多半是坐标系或权限问题;如果都偏,检查签名和参数顺序。这个土办法帮我省过好几次来回排查的时间。
做地图定位这几年,最大的教训就是「坐标系和权限这两件事,动手前不定死,后面全是后悔药」。希望帮到你。
本文还有配套的精品资源,点击获取