支持 H5 的浏览器提供了获取 GPS 的接口 navigator.geolocation.getCurrentPosition(success, error)
,由于各种原因,国内得到的定位都是不准确的,百度地图 API 也提供了定位接口 new BMap.Geolocation().getCurrentPosition(callback)
,不过也不准确,有时候会偏离几十公里,为了能够在浏览器里精确的定位,可以在微信浏览器中打开网页,使用微信的 JS SDK 中的 wx.getLocation()
进行定位得到大地坐标系 WGS84 的坐标,然后转换为百度 BD09 坐标,使用百度 API 获取此坐标对应的中文地址。
使用微信 JS SDK
微信 JS-SDK 是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。参考 JS-SDK 说明文档,调用 JS API 主要步骤如下:
- 注册微信公众号 (几分钟就可完成)
- 开启公众号的开发者模式:
开发 > 基本配置 > 启用
- 得到 AppID 和 开发者密码 AppSecret:
开发 > 基本配置 > 公众号开发信息
(关闭弹窗前要保存好 AppSecret,如果忘了就需要重新生成)
- 添加服务器的 IP 到公众号的白名单中:
开发 > 基本配置 > 公众号开发信息 > IP 白名单
,因为需要从服务器端向微信服务器发起 HTTP 请求获取 Access Token 和 Api Ticket
- 绑定使用此公众号开发的域名:
设置 > 公众号设置 > 功能设置 > JS 接口安全域名
(每个月只能修改 3 次)
- 生成网页中调用微信 JS API 需要的签名:
- 获取 Access Token (GET 请求): https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APP_ID&secret=APP_SECRET
- 使用上面得到的 Access Token 获取 JS API Ticket (GET 请求): https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=ACCESS_TOKEN
- 使用上面得到的 JS API Ticket 计算生成签名 signature (给
wx.config
使用),可以使用 微信 JS 接口签名校验工具 进行测试 (就是把参数拼成一个字符串使用 SHA1 进行编码得到签名)
- 网页中引入 JS 文件: https://res.wx.qq.com/open/js/jweixin-1.6.0.js
- 网页中调用 wx.config
1 2 3 4 5 6 7 8 9
| wx.config({ debug : false, appId : 'wxd499c94dd151d520', nonceStr : 'echo', timestamp: '1587879955', signature: '023b9930e2c1a7a8d6b15319b6a19efb1fde8364', jsApiList: ['getLocation'] });
|
- 在 wx.ready 中调用微信的 JS API
1 2 3 4 5 6 7 8 9
| wx.ready(function() { wx.getLocation({ type: 'wgs84', success: function (res) { var longitude = res.longitude; var latitude = res.latitude;
document.querySelector('#info').innerHTML = longitude + ', ' + latitude; } }); });
|
到这里就可以在微信的浏览器中使用微信提供的 JS API 了,具体都提供了哪些可用的 API,请参考 JS SDK 说明文档 中的附录 2 - 所有 JS 接口列表。此外,在开发中注意一下以下几点:
- Access Token 和 Api Ticket 的有效时间都是 2 小时,获取后可以先缓存起来,不要每次都请求新的
- signature 需要使用 Api Ticket 和访问微信 Api 的页面的 url 一起生成 (当前网页的 URL,不包含 # 及其后面部分)
- 签名用的 noncestr 和 timestamp 必须与 wx.config 中的 nonceStr 和 timestamp 相同
使用公众平台测试账号进行开发
微信需要通过域名访问我们的服务器,大多数情况下在开发的时候我们的电脑一般都没有独立 IP 和域名,为了让微信能够访问我们的开发电脑,可以借助内网映射工具如 ngrok, natapp 等映射一个域名到我们的电脑,把这个域名配置到上面第 5 步处要求的 JS 接口安全域名。由于每个月只能修改这个域名 3 次,为了安全起见,还可以使用微信提供的公众平台测试账号进行开发,在它里面设置开发时的临时域名,不用配置 IP 白名单,此外测试账号还开放了所有权限:
开发 > 开发者工具 > 公众平台测试账号
,可以获得 App ID 和 App Secret
接口配置信息
中填写 URL 和 Token,验证通过即可:
URL: 微信会访问这个 URL 验证我们的服务可不可用,只要在这个 URL 的响应中原样返回微信传来的参数 echostr 即可,例如我们的 URL 为 http://d2hp9x.natappfree.cc/api/demo/wechat/checkSignature,响应代码为
1 2 3 4 5
| @GetMapping("/api/demo/wechat/checkSignature") @ResponseBody public String checkSignature(@RequestParam String echostr) { return echostr; }
|
Token: 随便填就行 (与上面提到的 Access Token 没有任何关系)
微信关注下面的 测试号二维码
的公众号
在微信中访问我们使用了微信 JS API 的网页,不出意外,微信的 JS API 能够调用了
使用百度地图 SDK
请参考百度地图说明文档账号和获取秘钥,然后在网页中引入百度地图的 JS 就可以使用了。也可以直接使用下面的 <script src="https://api.map.baidu.com/api?v=2.0&ak=By5uc80cGiGYzgc0XUHGyDCoAGsiIi0x"></script>
,因为是免费无限制调用次数的。
下面附上相关代码作为参考。
网页中定位
下面的代码为使用微信定位得到 WGS84 的大地坐标系坐标,然后把其转换为百度的 BD09 坐标,然后获取位置的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>定位</title> </head> <body> <div id="wgs84-1"></div> <div id="wgs84-2"></div>
<div id="bd09-1"></div> <div id="bd09-2"></div>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <script src="https://api.map.baidu.com/api?v=2.0&ak=By5uc80cGiGYzgc0XUHGyDCoAGsiIi0x"></script> <script src="/static-x/lib/axios.min.js"></script> <script> const url = window.location.origin + window.location.pathname;
axios.get('/api/demo/wechat', { params: { url } }).then(result => { const config = result.data;
wx.config({ debug : false, appId : config.appId, nonceStr : config.nonceStr, timestamp: config.timestamp, signature: config.signature, jsApiList: ['getLocation'], }); });
wx.ready(function() { wx.getLocation({ type: 'wgs84', success: function (res) { var longitude = res.longitude; var latitude = res.latitude;
gpsToAddress(longitude, latitude).then(address => { document.querySelector('#wgs84-1').innerHTML = longitude + ', ' + latitude; document.querySelector('#wgs84-2').innerHTML = address; });
wgs84ToBD09(longitude, latitude).then(point => { document.querySelector('#bd09-1').innerHTML = point.longitude + ', ' + point.latitude;
return gpsToAddress(point.longitude, point.latitude); }).then(address => { document.querySelector('#bd09-2').innerHTML = address; }); } }); });
function wgs84ToBD09(longitude, latitude) { return new Promise((resolve, reject) => { const points = [new BMap.Point(longitude, latitude)];
new BMap.Convertor().translate(points, 1, 5, function(data) { const point = data.points[0]; resolve({ longitude: point.lng, latitude: point.lat }); }); }) }
function gpsToAddress(longitude, latitude) { return new Promise((resolve, reject) => { const point = new BMap.Point(longitude, latitude);
new BMap.Geocoder().getLocation(point, function (res) { const addComp = res.addressComponents; const address = addComp.province + addComp.city + addComp.district + addComp.street + addComp.streetNumber; resolve(address); }) }) } </script> </body> </html>
|
服务器端代码
Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Controller public class DemoController { @Autowired private WeChatService weChatService;
@GetMapping("/api/demo/wechat") @ResponseBody public Result<Map<String, String>> wechatConfig(@RequestParam String url) { return Result.single(weChatService.getJsApiConfig("your app id", "your app secret", url)); } }
|
在 Service 中请求 Access Token, API Ticket 和计算 Signature,其中
- 缓存使用了 JetCache
- Easy-OkHttp 发送 HTTP 请求
- FastJSON 解析 JSON
- Apache Commons Codec 计算 SHA1
以上几点都不难,大家可以根据项目中使用的库酌情进行替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alicp.jetcache.anno.CacheInvalidate; import com.alicp.jetcache.anno.Cached; import com.edu.training.bean.CacheConst; import com.edu.training.util.Utils; import com.mzlion.easyokhttp.HttpClient; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
import java.util.HashMap; import java.util.Map;
@Service @Slf4j public class WeChatService { @Autowired private WeChatService self;
private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APP_ID&secret=APP_SECRET";
private static final String URL_JS_API_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=ACCESS_TOKEN";
@Cached(name = CacheConst.CACHE, key = CacheConst.KEY_WECHAT_ACCESS_TOKEN) public String getAccessToken(String appId, String appSecret) {
log.info("[开始] 获取微信 Access Token: App ID {}", appId);
String url = URL_ACCESS_TOKEN.replace("APP_ID", appId).replace("APP_SECRET", appSecret); JSONObject result = JSON.parseObject(HttpClient.get(url).execute().asString()); String token = result.getString("access_token");
if (StringUtils.isBlank(token)) { String errorMessage = result.getString("errmsg"); log.warn("[错误] 获取微信 Access Token 失败: {}", errorMessage); }
self.invalidateJsApiTicket(appId);
log.info("[结束] 获取微信 Access Token: App ID {}", appId); return token; }
@Cached(name = CacheConst.CACHE, key = CacheConst.KEY_WECHAT_JS_API_TICKET) public String getJsApiTicket(String appId, String accessToken) { log.info("[开始] 获取微信 JS Api Ticket: App ID {}", appId);
if (accessToken == null) { log.warn("[错误] 获取微信 JS Api Ticket: Access Token 为 null"); return null; }
String url = URL_JS_API_TICKET.replace("ACCESS_TOKEN", accessToken); JSONObject result = JSON.parseObject(HttpClient.get(url).execute().asString()); String ticket = result.getString("ticket");
if (StringUtils.isBlank(ticket)) { String errorMessage = result.getString("errmsg"); log.warn("[错误] 获取微信 JS Api Ticket 失败: {}", errorMessage); }
log.info("[结束] 获取微信 JS Api Ticket: App ID {}", appId); return ticket; }
@CacheInvalidate(name = CacheConst.CACHE, key = CacheConst.KEY_WECHAT_JS_API_TICKET) public void invalidateJsApiTicket(String appId) { log.info("[成功] 删除微信 JS Api Ticket: App ID {}", appId); }
public Map<String, String> getJsApiConfig(String appId, String appSecret, String url) {
String accessToken = self.getAccessToken(appId, appSecret); String apiTicket = self.getJsApiTicket(appId, accessToken);
if (accessToken == null || apiTicket == null) { return null; }
if (StringUtils.isBlank(url)) { return null; }
String nonceStr = Utils.uuid().substring(0, 8); String timestamp = System.currentTimeMillis() + ""; String text = "jsapi_ticket=" + apiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; String signature = Utils.sha1(text);
Map<String, String> config = new HashMap<>(); config.put("appId", appId); config.put("nonceStr", nonceStr); config.put("signature", signature); config.put("timestamp", timestamp);
return config; } }
|