在微信公众号开发中,获取用户地理位置是一个高频需求。无论是附近门店查询、基于位置的签到,还是同城配送计算距离,都需要拿到用户的经纬度。如果单纯让用户在输入框里填写地址,体验差且坐标不精确。微信公众号自定义菜单提供了一个专门的类型——location_select,用户点击菜单后,微信客户端会直接弹出系统级的地理位置选择器,用户从地图上选一个点,确认后微信服务器就会把该点的经纬度推送给开发者服务器。整个过程不需要额外申请定位权限,也不需要前端调用任何地图SDK,实现成本很低。

本文会从菜单类型的原理讲起,详细说明如何通过官方接口创建带有location_select按钮的自定义菜单,以及服务器端如何接收并解析LOCATION事件。代码示例使用PHP,因为这是公众号开发中使用最广泛的语言,其他语言的开发者可以参照接口流程进行迁移。需要注意,location_select事件推送中没有EventKey字段,这意味着如果创建了多个位置选择菜单,服务器无法直接区分是哪个菜单触发的,实际项目里一般只保留一个此类菜单,或者配合其他交互逻辑来区分。
location_select菜单类型的触发机制
微信公众号自定义菜单支持多种按钮类型,常用的有click(点击推事件)、view(跳转URL)、scancode_push(扫码推事件)、location_select(弹出地理位置选择器)等。其中location_select是专门为获取用户位置设计的类型,用户在微信内点击该菜单后,不会触发网页跳转,也不会直接给服务器发送一个普通点击事件,而是在微信客户端内部打开一个地图选择器页面。用户可以拖动地图、搜索地点,或者直接使用当前定位,然后点击发送。只有用户确认发送位置后,微信服务器才会向开发者配置的服务器地址推送一条事件消息,事件类型Event的值为LOCATION。
与click类型事件不同的是,LOCATION事件的XML消息体中不包含EventKey字段。click事件会带上key值,服务器根据key区分用户点了哪个按钮;而location_select事件只携带用户选择的位置坐标信息,字段包括Latitude(纬度)、Longitude(经度)和Precision(精度,单位为米)。这意味着如果公众号菜单里同时放置了多个location_select按钮,用户点击任意一个,推送的事件结构完全一样,服务器端无法判断具体是哪个菜单触发的。因此在实际开发中,一般不会同时配置多个位置选择菜单,如果需要多个入口,可以先让用户点击一个click菜单,再回复一个包含位置选择链接的消息,或者直接使用微信JS-SDK的getLocation接口。
还需要理解的是,如果用户在位置选择器页面点击了取消,微信客户端不会向服务器发送任何事件,也就是说服务器根本不知道用户曾经打开过选择器。这一点在业务设计时需要提前考虑,比如用户点击菜单后长时间没有收到事件,需要给用户一些提示,引导重新点击。另外,LOCATION事件推送的坐标是基于WGS84标准,而不是国内常用的GCJ-02加密坐标,如果后续要在地图上展示或者计算距离,需要注意坐标系的转换。
通过接口创建自定义菜单
创建自定义菜单需要先获取接口调用凭证access_token,然后调用菜单创建接口。菜单结构是一个JSON对象,每个一级菜单或二级菜单都是一个button对象,button的type字段指定菜单类型。对于location_select类型,type值为location_select,同时必须设置name(菜单标题)和key(菜单标识,虽然事件推送中不返回key,但接口要求必填)。下面是一段PHP代码,演示如何构造菜单JSON并调用创建接口。
<?php
// 获取access_token,实际项目中建议缓存,避免频繁请求
$appid = '你的AppID';
$secret = '你的AppSecret';
$tokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}";
$tokenResult = json_decode(file_get_contents($tokenUrl), true);
$accessToken = $tokenResult['access_token'] ?? '';
if (!$accessToken) {
echo '获取access_token失败';
exit;
}
// 菜单结构,这里只配置一个location_select按钮
$menuData = [
'button' => [
[
'name' => '我的位置',
'type' => 'location_select',
'key' => 'get_location',
],
// 可以继续添加其他菜单,比如click类型
[
'name' => '关于我们',
'type' => 'click',
'key' => 'about_us',
],
],
];
$menuJson = json_encode($menuData, JSON_UNESCAPED_UNICODE);
$createUrl = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token={$accessToken}";
// 使用cURL发送POST请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $createUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $menuJson);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
echo "\n菜单JSON:\n" . $menuJson;
?>
上述代码中,菜单JSON使用了JSON_UNESCAPED_UNICODE选项,这样中文字符不会转义成unicode编码,方便调试查看。创建成功后,微信返回的JSON中errcode为0。需要特别注意的是,菜单key在同一个公众号下必须唯一,即使location_select事件不含key,接口创建时也不能与其他菜单的key重复,否则会报错。另外,一级菜单最多3个,二级菜单最多5个,超出限制会导致创建失败。
如果使用测试号,也可以在微信公众平台后台的可视化界面直接配置菜单,但接口方式更适合程序化管理、动态更新菜单。通过接口创建菜单后,微信客户端会在用户重新进入公众号会话时刷新菜单,一般几分钟内生效。在开发调试阶段,建议先删除旧菜单再创建新菜单,避免缓存导致测试结果不准确。删除菜单接口为:GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN。
接收并解析LOCATION事件
用户点击定位菜单并选择位置后,微信服务器会向公众号配置的服务器地址发送一个POST请求,请求体为XML格式。一个典型的LOCATION事件XML如下所示。
<xml> <ToUserName><![CDATA[公众号原始ID]]></ToUserName> <FromUserName><![CDATA[用户OpenID]]></FromUserName> <CreateTime>1712345678</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[LOCATION]]></Event> <Latitude>23.137466</Latitude> <Longitude>113.352425</Longitude> <Precision>119.385040</Precision> </xml>
服务器收到请求后,需要先验证签名(如果是明文模式可以只处理事件),然后解析出Event字段,判断是否为LOCATION,再提取经纬度和精度。下面是一段完整的PHP处理代码,包括签名验证、XML解析和回复文本消息。实际部署时,应将代码放在公众号后台配置的服务器URL对应的入口文件中。
<?php
// 服务器地址入口文件,例如 https://yourdomain.com/wechat.php
define('TOKEN', '你公众号后台配置的Token');
// 获取微信服务器发送的数据
$postStr = file_get_contents('php://input');
if (empty($postStr)) {
echo 'no data';
exit;
}
// 验证签名
$signature = $_GET['signature'] ?? '';
$timestamp = $_GET['timestamp'] ?? '';
$nonce = $_GET['nonce'] ?? '';
$tmpArr = [TOKEN, $timestamp, $nonce];
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($signature !== $tmpStr) {
// 签名验证失败,记录日志后可返回空
echo '';
exit;
}
// 解析XML
libxml_disable_entity_loader(true);
$xmlObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($xmlObj === false) {
echo 'success';
exit;
}
$msgType = (string)$xmlObj->MsgType;
$event = (string)$xmlObj->Event;
$fromUser = (string)$xmlObj->FromUserName;
$toUser = (string)$xmlObj->ToUserName;
if ($msgType === 'event' && $event === 'LOCATION') {
$latitude = (float)$xmlObj->Latitude;
$longitude = (float)$xmlObj->Longitude;
$precision = (float)$xmlObj->Precision;
// 在这里处理业务逻辑,比如保存位置、查询附近门店等
// 这里仅演示回复用户一段文本
$content = "已收到你的位置:纬度 {$latitude},经度 {$longitude},精度 {$precision} 米";
} else {
$content = '点击菜单可获取位置信息';
}
// 回复文本消息
$replyXml = "<xml>
<ToUserName><![CDATA[{$fromUser}]]></ToUserName>
<FromUserName><![CDATA[{$toUser}]]></FromUserName>
<CreateTime>" . time() . "</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{$content}]]></Content>
</xml>";
echo $replyXml;
exit;
?>
代码中先验证了签名,保证请求确实来自微信服务器。验证通过后使用simplexml_load_string解析XML,LIBXML_NOCDATA参数可以自动将CDATA内容转换为字符串。判断MsgType为event且Event为LOCATION后,提取Latitude、Longitude、Precision三个字段。回复的XML中要特别注意字段顺序,并且ToUserName要写用户的OpenID,FromUserName写公众号的原始ID,两者不要写反。回复的内容需要包在CDATA中,避免特殊字符导致XML解析错误。
关于Precision字段,它表示定位精度,单位是米。比如Precision为119.385040,意味着定位误差大约在119米左右。这个值越小说明定位越准确,实际业务中可以根据精度做过滤,例如精度大于500米的坐标可以提示用户重新选择。另外,坐标是WGS84标准,如果后续使用高德或腾讯地图展示,需要转换为GCJ-02坐标,转换算法网上有现成实现,或者直接使用地图服务商提供的坐标转换API。由于location_select事件不包含EventKey,如果业务上必须区分多个位置菜单入口,可以考虑在用户点击前先让其点击一个click菜单,服务器回复一个提示,引导用户点击特定的位置菜单,或者干脆只保留一个位置入口。
开发中的常见问题与调试建议
第一个常见问题是用户取消选择器后收不到任何事件。前面提到,微信不会推送取消操作,服务器无法感知。为了优化体验,可以在用户点击菜单后,服务器暂时无法得知,所以更好的做法是在菜单名称上做提示,例如叫“发送我的位置”,让用户明确知道点击后要选择一个点并发送。同时,如果业务对位置获取成功率要求很高,可以结合客服消息或者模板消息,在用户长时间未操作时主动提醒,但实现起来较为复杂。另一个思路是放弃location_select,改用JS-SDK的getLocation接口,由网页自己处理取消和超时逻辑,这样可控性更强,但需要用户授权网页定位权限,且必须使用认证服务号。
第二个问题是测试环境的配置。公众号开发需要公网可访问的服务器地址,本地调试可以使用内网穿透工具。配置服务器URL时,Token需要与代码中的TOKEN常量保持一致,EncodingAESKey如果使用明文模式可以不填。验证通过后,才能正常接收事件推送。调试过程中,可以在入口文件开头将$postStr写入日志文件,方便查看微信实际推送的XML内容。日志文件路径需要注意服务器写权限,比如在Linux环境下可以写到/tmp/wechat.log。
第三个问题是坐标数据的使用。拿到经纬度后,通常会保存到数据库,后续做附近推荐或者距离计算。如果使用的是MySQL,可以新建一张用户位置表,字段包含openid、latitude、longitude、precision、create_time等。计算两个坐标点之间的距离可以使用Haversine公式,但注意数据库中的经纬度要按照浮点数存储,不要存成字符串,以免排序和计算出错。对于高并发场景,频繁写入位置信息可能造成数据库压力,可以考虑先写入Redis,再异步批量落库。此外,用户可能多次点击定位菜单,每次都会收到新坐标,业务上需要决定是以最新一次为准,还是记录历史轨迹,这些都需要根据产品需求提前设计好。
最后要强调的是,location_select菜单虽然方便,但只能获取用户主动选择的位置,并非实时GPS定位。用户可以选择地图上任意一点,甚至不是自己所在的位置,所以不要将这种数据用于需要强身份验证的场景,比如基于位置的考勤打卡,用户可以作弊。如果需要更高可信度的位置,可以结合手机号验证或拍照等方式。但对于大多数基于位置的推荐和展示类业务,location_select已经能够满足需求。