尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
微信小程序LBS实战:从定位到附近美食POI检索与排序
简介微信小程序开发学习者可参考这份完整的附近美食餐厅查询案例资源围绕地图定位、餐厅列表展示与信息详情等核心页面展开适合初步掌握小程序语法、希望结合真实场景练习前后端交互的读者。压缩包共39个文件体积仅99KB代码量精简而结构完整12个JSON文件负责全局配置与页面配置10个JS文件承载业务逻辑与接口调用6个WXSS和4个WXML分别完成样式布局与页面骨架7张PNG图片提供餐厅及图标素材另含项目配置文件可直接导入开发者工具运行。目前已有534人学习下载。通过此案例可学习小程序目录组织方式、底部导航配置、附近位置数据获取与展示思路以及libs工具库的封装方法简单明了的项目结构也便于二次改造适合作为课堂作业、毕业设计或自学练手的起点。1. 附近的美食餐厅是定位问题不是搜索问题微信小程序实现查看附近的美食餐厅听起来在谈搜索落到代码里其实是三个独立问题坐标从哪来、餐厅数据从哪来、距离怎么算。真正决定体验的不是列表 UI而是定位精度和坐标系是否一致。下面按实际开发顺序给出闭环方案wx.getLocation 取用户坐标并统一到 gcj02腾讯位置服务按坐标做圆内关键词检索前端再按真实距离排序渲染。适合已经能写基础页面、想补完整 LBS 能力的开发者做完之后把关键词换成酒店、停车场同一套代码还能复用。2. 用 wx.getLocation 拿坐标再用 haversine 计算真实距离2.1 先统一坐标系gcj02 才是小程序的“附近”国内的地图服务普遍存在坐标系偏移问题微信小程序也不例外。wx.getLocation默认返回 wgs84 原始坐标而小程序里的map组件、腾讯位置服务 API 都是基于 gcj02。如果两者混用餐厅标点会整体偏移几十米到上百米表现就是地图上看起来“就在旁边”的店点开发现隔了一条街。很多“附近美食”肉眼可见的异常不是算法不行而是坐标系不统一。这个环节不需要做任何坐标换算只要记住一条原则取坐标时显式传type: gcj02后续把这份坐标原样传给搜索接口和地图组件。gcj02 是国测局加密坐标适合在国内地图服务间流转。自己写转换公式反而容易引入精度误差尤其是那些从第三方拿到的火星坐标转换代码参数版本参差不齐结果比不转还难排查。2.2 拉取坐标的最小可运行代码与参数选择一个可以直接放进 Page 的定位方法最少长这样function getPosition() { return new Promise((resolve, reject) { wx.getLocation({ type: gcj02, // 使用 gcj02与 map 组件、腾讯位置服务保持一致 altitude: false, // 不取海拔减少一次计算 isHighAccuracy: true, // Android 上启用高精度定位 highAccuracyExpireTime: 4000, // 高精度超时时间4 秒内拿不到就降级 success: (res) { resolve({ latitude: res.latitude, longitude: res.longitude, accuracy: res.accuracy || 0 }) }, fail: (err) reject(err) }) }) }type: gcj02是本段最关键的参数它保证后续所有距离计算和地图标注不需要二次转换。altitude: false只关闭海拔通道不影响水平定位。isHighAccuracy只对 Android 生效开启后首次定位可能多等半秒到两秒所以配highAccuracyExpireTime: 4000做超时降级避免用户长时间盯着 loading。accuracy是系统返回的估算精度单位米后面修正搜索半径时会用到很多人会忽略这个字段。还需要在app.json里声明权限用途{ permission: { scope.userLocation: { desc: 你的位置信息将用于查找附近美食餐厅 } } }不写这段描述部分基础库版本会在授权弹窗阶段直接报getLocation:fail system permission denied。desc里的文字会原样展示在授权弹窗中写成用户能看懂的一句话而不是“用于定位”这种模糊表述。2.3 用 haversine 做距离校验接口返回的距离先别全信腾讯位置服务在检索结果里会带_distance字段这个字段在大多数场景下够用但它只代表服务端按查询点算出来的直线距离不会考虑定位误差和坐标抖动。分页加载时如果完全信任它偶尔会出现第一页末尾的店比第二页开头的店更远、排序断层。稳妥做法是在前端用 haversine 公式把用户坐标和店铺坐标再算一遍function calcDistance(from, to) { const R 6371000 // 地球半径单位米 const rad Math.PI / 180 const dLat (to.latitude - from.latitude) * rad const dLng (to.longitude - from.longitude) * rad const sinLat Math.sin(dLat / 2) const sinLng Math.sin(dLng / 2) const h sinLat * sinLat Math.cos(from.latitude * rad) * Math.cos(to.latitude * rad) * sinLng * sinLng return Math.round(R * 2 * Math.asin(Math.sqrt(h))) }返回单位是米保留整数即可。两个注意点第一Math.asin的入参可能因浮点误差略大于 1严谨一点可以写成Math.asin(Math.min(1, Math.sqrt(h)))第二不要在 WXML 里逐条调用这个函数而要在拿到列表后批量计算完再setData否则页面滚动会掉帧。2.4 定位缓存、超时与失败降级用户只是在这几条街来回切换“附近餐厅”每次冷启动都重新定位会产生两到三秒白屏。常见做法是把坐标缓存到本地下一次先渲染缓存function getCachedPosition(expire 5 * 60 * 1000) { const cache wx.getStorageSync(nearby_position) if (cache Date.now() - cache.ts expire) { return cache } return null }缓存时间按业务场景定5 分钟适合“逛一圈回来再看”的强度骑行、跑步这类会快速移动的场景缓存应缩到 1 分钟以内。定位失败不一定都是用户拒绝也可能是信号差或高精度超时失败回调里要先wx.getSetting查scope.userLocation是否为 true。如果权限已开启但定位失败去掉高精度参数再试一次如果权限本身被关再走设置页引导。3. 腾讯位置服务关键字搜索美食 POI 的请求参数与返回结构3.1 选腾讯位置服务不只是因为坐标系一致“附近餐厅”的 POI 数据可以来自自家后台、第三方聚合接口、腾讯位置服务。如果自建餐厅表得自己维护坐标索引、分类标签、营业状态数据量少时看不出问题店一多就变成运维负担。第三方聚合接口通常要签约计费适合门店数量较大的商户端。中小型小程序的主流做法是直接调腾讯位置服务的 WebService API理由有两个一是它和小程序map组件同属 gcj02坐标即取即用二是返回字段里已包含店名、地址、类目、电话、距离足够拼出列表页和地图页。另一个实际优势是域名固定一个合法域名就能覆盖所有基础检索接口。3.2 上线前必做的两个配置request 合法域名和 key 权限开发版调试时小程序默认不校验域名很多项目在开发者工具里一切正常一上传体验版就报url not in domain list。上线前要完成两个配置配置项操作位置具体内容request 合法域名小程序管理后台 → 开发 → 开发设置 → 服务器域名添加https://apis.map.qq.comWebServiceAPI Key腾讯位置服务控制台 → 应用管理创建 Key 并开启 WebServiceAPI 能力这里要补充的是合法域名校验只盯域名不盯路径apis.map.qq.com下的 place 搜索、逆地址解析等接口共用这一个域名。如果你项目里同时用了同一域名下的不同产品不需要重复添加。线上号和小程序关联时尽量不要用测试 Key避免上线后 Key 额度不足时在线上打满流控。3.3 用 boundarynearby 请求周边餐厅请求示例和字段说明请求可以放在云函数里发也可以放到自己的后端。没有后端的情况下可以先把代码放到云函数避免把 Key 写进小程序包。云函数中的请求代码大致如下// 云函数: nearbyFood/index.js const axios require(axios) exports.main async (event) { const { lat, lng, radius 3000, page 1 } event const url https://apis.map.qq.com/ws/place/v1/search const params { keyword: 美食, boundary: nearby(${lat},${lng},${radius}), page_size: 20, page_index: page, orderby: _distance, key: 你的WebServiceAPIKey } const resp await axios.get(url, { params }) const body resp.data if (body.status ! 0) { throw new Error(body.message) } return body }各参数在实际项目里这样调参数示例值说明keyword美食可换成餐厅、火锅等由业务入口决定boundarynearby(31.23,121.47,3000)依次为纬度、经度、半径半径单位米page_size20该接口单页最多返回 20 条page_index1从 1 开始递增orderby_distance服务端按距离由近到远排序key控制台生成的 Key正式环境不要出现在前端代码里boundarynearby(lat,lng,radius)是“真附近”的过滤条件。半径不建议一开始给到 50 公里那会把“附近”变成“整座城市”排序失去意义。页面一次展示 10 到 20 家足够把 radius 控制在 3000 到 5000 之间配合page_size20是大多数美食场景的平衡点。orderby_distance只保证当前页有序翻页后不同页之间的相对顺序需要前端重新排下一章会展开讲。返回体结构是status、count、data三段式。status为 0 表示成功data才是 POI 数组每条数据类似{ id: 851885789898, title: 老街坊本帮菜(人民广场店), address: 人民大道 100 号, category: 美食中餐厅本帮菜, telephone: 021-66668888, location: { lat: 31.234, lng: 121.472 }, _distance: 425.3 }_distance单位是米可以直接展示为“约 425 米”。location.lat和location.lng是 gcj02 坐标给小程序map组件用正好。category可以做前端分类筛选比如只展示火锅、只展示快餐。telephone经常是空字符串页面要有空态兜底别默认每家店都有电话。3.4 把搜索响应解析成页面可用的店铺模型直接往页面扔原始数组不是不行但后期改字段、加排序会很别扭。我习惯在云函数里做一次瘦身只返回页面需要的字段顺便补一个distanceTextconst cleanList (body.data || []).map((poi) ({ id: poi.id, title: poi.title, address: poi.address, category: poi.category, latitude: poi.location.lat, longitude: poi.location.lng, distance: poi._distance || 0, telephone: poi.telephone }))id是去重关键。关键词搜索可能在同一个位置返回多家相似 POI多翻几页后同一家店会重复出现。实际操作中我会在合并前用id建一个 Map后拿到的覆盖先拿到的。瘦身后的数组比原始 JSON 小很多setData数据量低页面渲染更快也不容易误暴露服务端内部字段。4. 餐厅列表卡片、距离排序与地图标点的前端实现4.1 页面数据流定位、请求、合并的统一入口把定位、请求、合并拆成三个独立函数比全写在onLoad里更容易调试。以index.js为例Page({ data: { list: [], page: 1, hasMore: true, loading: false, userLat: null, userLng: null }, async onLoad() { try { const pos await getPosition() this.setData({ userLat: pos.latitude, userLng: pos.longitude }) const newList await this._fetchList(pos) this._mergeList(newList) } catch (err) { this.handleLocationDenied(err) } }, async _fetchList(pos) { const res await wx.cloud.callFunction({ name: nearbyFood, data: { lat: pos.latitude, lng: pos.longitude, page: this.data.page } }) return res.result.data || [] }, _mergeList(newItems) { const map new Map() this.data.list.forEach(item map.set(item.id, item)) newItems.forEach(item map.set(item.id, item)) const sorted [...map.values()].sort((a, b) a.distance - b.distance) this.setData({ list: sorted }) } })定位成功后才发请求避免用户没授权也白打一次接口。_fetchList只负责拉取当前页数据_mergeList负责去重和排序职责清晰。注意wx.cloud.callFunction返回的是res.result这里直接把它当成了云函数里返回的{ data: [...] }结构如果换了后端字段名这一层的映射也要同步改。4.2 餐厅卡片的 WXML 与距离格式化函数列表 WXML 不需要复杂排版信息主次清楚即可。一张卡片放店名、距离、地址、类目最多加一个电话入口。view classpoi-card wx:for{{list}} wx:keyid view classpoi-header text classpoi-name{{item.title}}/text text classpoi-distance{{formatDistance(item.distance)}}/text /view view classpoi-address{{item.address || 地址暂缺}}/view view classpoi-category{{item.category}}/view /view距离格式化建议做成 Page 方法因为 WXML 内联调用不好处理复杂逻辑formatDistance(d) { if (!d d ! 0) return 距离未知 return d 1000 ? d m : (d / 1000).toFixed(1) km }formatDistance必须处理undefined和0。腾讯位置服务偶尔会缺失_distance直接调用toFixed会抛错。地址为空时展示兜底文案避免整张卡片信息残缺。类目字段很可能很长例如“美食中餐厅本帮菜”UI 上可以只保留第一级“美食”或最后一级“本帮菜”通过split().pop()取最后一段即可。4.3 距离排序的回归处理翻页后不能信 orderby接口带了orderby_distance但分页场景下第一页和第二页来自两次独立排序顺序不一定连续而且去重后可能少掉一条导致下一页出现比上一页更近的店。因此前端必须在合并后做一次全量排序上面的_mergeList里的sort就是这道保险。如果业务还要按评分、热度排序就不能再用_distance做全量排序常见做法是越权概率排序先按评分分组再在组内按距离升序。这个需求别放在前端硬算应当把排序字段传进云函数让服务端返回排好序的列表前端只做稳定性兜底。分页放在onReachBottom里async onReachBottom() { if (this.data.loading || !this.data.hasMore) return this.setData({ page: this.data.page 1 }) const pos { latitude: this.data.userLat, longitude: this.data.userLng } const result await this._fetchList(pos) this._mergeList(result) this.setData({ hasMore: result.length 0 }) }hasMore用本次返回结果是否为空判断比先请求总条数再比较省一次接口调用。loading标志位防止onReachBottom在 iOS 上连续触发两次。4.4 用 map 组件把餐厅和用户位置画到同一张图上列表之外很多“附近”场景会加一个地图页展示空间关系。小程序map组件接受markers数组把上面的坐标数据传入即可map classfood-map latitude{{userLat}} longitude{{userLng}} markers{{markers}} scale15 show-location /const markers this.data.list.map((item, index) ({ id: index, latitude: item.latitude, longitude: item.longitude, title: item.title, callout: { content: item.title, display: BYCLICK }, width: 32, height: 32 })) this.setData({ markers })scale15对应 1 公里级视野适合餐厅密度高的商圈。show-location会在图上画蓝点表示用户真实位置省掉一个自定义用户 marker。注意 markers 的id必须是数字不能直接用item.id字符串所以用 index 重写。点击 marker 弹出店名气泡再点击气泡就可以用wx.openLocation把用户带去导航这是成本最低的接入导航方式。5. 精度误差、权限拒绝与 key 安全上线前留意的 4 个细节5.1 搜索半径要吸收定位误差getLocation返回的accuracy在开阔地带可能 30 米在商圈可能是 80 米甚至更高。如果搜索半径设得很紧用户站在美食街中间但误差是 90 米radius 填 100最终可能只返回两三家店。把 radius 在传入云函数前按误差放大通常这样算const fixedRadius Math.max(radius, Math.ceil(radius * 1.1) accuracy)这样既不会因为定位抖动丢结果也不会把半径放得太大导致全是远距离店。5.2 用户拒绝授权后的二次引导微信只会在第一次调用时弹出授权框用户拒绝后不会再次自动弹出必须在页面留一个带说明的入口按钮。点击后用wx.openSetting打开设置页wx.getSetting({ success: (res) { if (!res.authSetting[scope.userLocation]) { wx.openSetting({}) } } })用户打开开关回到小程序页面onShow里要重新触发定位和拉取。注意这个入口要显式放在页面上例如“需要定位才能查看附近餐厅”的按钮不能一进页面自动弹设置页否则容易被平台判定为骚扰用户。5.3 key 不要出现在小程序包内联调时可以临时把 key 写在前端 config.js正式发布前要迁到云函数或自己的后端。小程序包可以被反编译key 一旦泄露别人可以直接消耗你的配额。云函数里再对调用方openid做限流同一用户一分钟内超过 20 次直接拒绝比只依赖 key 本身安全得多。5.4 用缓存命中减少重复请求同一地点 10 分钟内反复进入页面不必每次都重新定位和请求。把定位结果和列表一起setStorageSync下次命中缓存直接渲染再静默更新。缓存 key 带上坐标取整值例如nearby_${Math.round(lat * 100)}_${Math.round(lng * 100)}坐标移动超过一定范围自动掉缓存。缓存版本号拼到 key 尾部发布新版时可强制所有旧缓存失效。本文还有配套的精品资源点击获取
RELATED

相关推荐

C/C++实现散列表通讯录:散列函数与冲突处理详解

C/C++实现散列表通讯录:散列函数与冲突处理详解

简介:基于C/C语言散列表实现的通讯录系统课程设计资料包,面向需要完成课程设计、大作业或初学散列表的计算机专业学生。项目以电话号码和用户名为关键字分别建立散列表,实现记录录入、冲突处理、按号码或姓名快速查找与显示,并进一…

📅 2026/9/16 16:08:50
Electron+SQLite构建桌面CRM:架构设计、数据模型与实战踩坑记录

Electron+SQLite构建桌面CRM:架构设计、数据模型与实战踩坑记录

DeskcommCRM 是我业余时间维护的一个桌面端客户关系管理系统,名字拆开看就是 Desk Comm CRM:把日常办公桌面上的客户资料、沟通记录、销售跟进串在同一个本地工作台里。最早做它不是想造轮子,而是当时团队实在太痛了——销售手里一堆客户 E…

📅 2026/9/16 16:08:50
Spring Boot智能健康饮食系统实战:从数据库设计到前后端部署

Spring Boot智能健康饮食系统实战:从数据库设计到前后端部署

简介:一套面向毕业设计与课程作业的智能健康饮食系统完整项目资源,基于Java与SpringBoot框架实现,覆盖用户信息、健康数据、饮食记录等核心业务模块。压缩包共352个文件,含88个Java后端代码、74个Vue前端组件、40个JS脚本&#xf…

📅 2026/9/16 16:08:50
MORE NEWS

更多资讯

📰

在 Linux 上使用 rbenv 安装与管理 Ruby:完整安装指南

在 Linux 上使用 rbenv 安装与管理 Ruby:完整安装指南 【免费下载链接】curriculum The open curriculum for learning web development 项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum 本文档对应 The Odin Project 开源课程(本仓…

📰

Rekor 可插拔类型(Pluggable Types)机制深度解析:透明日志条目 Schema 插件体系与 BuildKit 中的落地实践

Rekor 可插拔类型(Pluggable Types)机制深度解析:透明日志条目 Schema 插件体系与 BuildKit 中的落地实践 【免费下载链接】buildkit concurrent, cache-efficient, and Dockerfile-agnostic builder toolkit 项目地址: https://gitcode.co…

📰

长沙市POI数据处理实战:从7z解压到DEM叠加分析

简介:一份面向GIS分析、城市规划、商业选址与交通研究人员的长沙市2020年POI数据集包,内含30米分辨率DEM数字高程模型、长沙市区县/街道等行政区划边界,以及shp和Excel两种格式的兴趣点数据。POI覆盖餐饮、购物、医疗保健、政府机构、住宿服务…

📰

C++实现结构光激光中心线亚像素提取

简介:本资源是一套基于C实现的线结构光视觉传感器标定核心代码,面向机器视觉、工业检测及光学测量方向的研究生与工程师,聚焦激光光条中心线的高精度提取问题。项目完整实现了多格式图像解码(BMP/JPG/PCX/GIF)、大津法…

📰

Carbon Web Components 的 `cds-code-snippet` 渲染原理与快照测试深度解析

Carbon Web Components 的 cds-code-snippet 渲染原理与快照测试深度解析 【免费下载链接】carbon A design system built by IBM 项目地址: https://gitcode.com/GitHub_Trending/carbo/carbon 导读 cds-code-snippet 是 IBM Carbon Design System 在 Web Components …

📰

mlx-audio 中的 Dramabox 语音合成:基于 LTX DiT + Gemma 编码器的 48 kHz 立体声 TTS 与参考音频克隆实现

mlx-audio 中的 Dramabox 语音合成:基于 LTX DiT Gemma 编码器的 48 kHz 立体声 TTS 与参考音频克隆实现 【免费下载链接】mlx-audio A text-to-speech (TTS), speech-to-text (STT) and speech-to-speech (STS) library built on Apples MLX framework, providing…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬