返回探索
指南VibeFix 编辑部更新于 2026年10月17日

“附近的咖啡馆”怎么做:vibe 项目的位置与地图功能实战

从“附近的咖啡馆”到骑手实时位置:入库只存 WGS84、展示层再转 GCJ-02/BD-09,附近搜索走粗排精排两阶段,地图 SDK 先看合规再看价格,逆地理编码按 geohash 缓存,精确位置从采集侧就当敏感个人信息对待。

城市高空俯拍照片:暖色暮光下的密集街区与高楼,隐喻城市位置服务

先划清边界:这篇不讲什么

位置功能容易和三篇已发指南混淆,先说明分工:

  • 《vibe 项目的站内搜索实战》讲的是「文本和向量的检索」——分词、倒排索引、向量召回。本篇讲的是「地理空间的检索」:坐标怎么存、附近怎么查、距离怎么排。索引结构、排序逻辑、性能瓶颈完全不同,不是一回事。
  • 《vibe 项目的日历预约与时区实战》讲的是「时间维度」——RRULE、时区三铁律。本篇讲的是「空间维度」。两者只在"营业中 + 离我近"这种联合筛选中交汇,方法论各自独立。
  • 《数据主体请求(DSR)实战》讲的是用户行使删除权时你怎么办。本篇讲的是采集侧的最小化:位置是敏感个人信息,怎么少存、模糊存、定期删。两篇互补,不重叠。

本篇只回答一个问题:从"附近的咖啡馆"到"外卖骑手实时位置",一人团队怎么做位置功能不踩坑。坐标系、附近搜索、地图 SDK 选型、地理编码、隐私合规——全是今天就能动手的部分。

第一课:坐标系——中国场景的必考题

经纬度不是只有一个标准,这个坑不填,后面全错:

  • WGS84:GPS 原始坐标,国际通用。你的数据库里应该只存这一种。
  • GCJ-02(火星坐标系):中国大陆境内,地图 SDK(高德、腾讯)用的都是它。从 WGS84 到 GCJ-02 有几百米的系统性偏移——直接把 GPS 坐标画在高德地图上,点会飘到马路对面去。
  • BD-09:百度在 GCJ-02 上又加了一层偏移。只在用百度地图 SDK 时打交道。

铁律:入库统一存 WGS84,展示时按所用地图 SDK 实时转换。转换算法是公开的(几十行代码),别存两套坐标自找麻烦。海外项目没有这个问题,但 SDK 选型时确认它吃哪种坐标系依然必要。

精度怎么存:PostgreSQL 里用 geography(Point, 4326) 类型,别用两个 float 裸存。geography 按球面算距离(单位米),geometry 按平面算——跨城市查询时后者误差肉眼可见。MySQL 用户用 POINT + SPATIAL 索引,是一样的道理。

核心功能:附近搜索——粗排与精排两阶段

附近搜索两阶段流程图:粗排(用户坐标、geohash 编码、九宫格前缀匹配)→精排(候选集、ST_Distance/Haversine 精确算距、距离游标分页),附 geohash 长度与网格精度对照表

"找我附近 3 公里的咖啡馆"听起来简单,数据量上百万行时 naive 实现直接把数据库查死。正确姿势是两阶段:

  • 阶段一 · 粗排(索引层):把用户坐标编码成 geohash(如 wx4g0e),取 5~6 位前缀,用普通 B-tree 做前缀匹配,一次把候选集从百万行砍到几百行。注意取九宫格(自身网格 + 周围 8 个):用户如果站在网格边界上,只查自身网格会漏掉马路对面的店。
  • 阶段二 · 精排(计算层):对几百个候选精确算球面距离(ST_DWithin / ST_Distance,或应用层 Haversine 公式),按距离排序。
  • 为什么分两阶段:精确距离计算是 CPU 密集型,对百万行逐行算是自杀。用前缀索引先砍候选集,再精算——这是所有 LBS 系统的通用形状。
-- PostGIS 一句话附近搜索(geography 类型 + GIST 索引是前提)
SELECT id, name,
       ST_Distance(location, ST_MakePoint(116.4, 39.9)::geography) AS dist_m
FROM shops
WHERE ST_DWithin(location, ST_MakePoint(116.4, 39.9)::geography, 3000)
ORDER BY dist_m
LIMIT 20;
-- GIST 索引:CREATE INDEX ON shops USING GIST (location);
-- 没有这个索引,上面的查询就是全表扫描
  • 分页别用 offset:"附近"列表的排序键是距离(连续变化的),用 WHERE dist_m > :last_dist 做游标。offset 在"第 5 页"时要先算完前 4 页的距离,越翻越慢。
  • geohash 方案 vs PostGIS:已有 PostgreSQL 直接上 PostGIS(最省事);用 MongoDB / MySQL 或 serverless 数据库没有空间索引时,geohash 前缀列是零依赖的替代方案。两种都吃透,换技术栈不慌。

地图 SDK 选型:先看合规,再看价格

方案适合注意
高德 / 腾讯位置服务国内用户为主GCJ-02 坐标系;按日调用量计费,免费额度对小项目够用
Mapbox海外用户、要高度自定义样式按地图加载次数计费;样式自由度最高
Google Maps海外、要地点数据(POI)最全最贵;国内不可用
Leaflet + OSM 瓦片预算为零、能接受自建瓦片免费但 POI/地理编码要自己解决
  • 一人团队的默认答案:国内用高德(文档中文、免费额度实在),海外用 Mapbox(样式好看、vibe 项目气质搭)。Google Maps 是"用户量起来之后再说"的选项。
  • 瓦片可以自托管:地图底图本质是图片瓦片,OSM 数据 + TileServer-GL 自己切,一个 5 美元的 VPS 能扛住小流量。这是《自托管实战》思路在地图场景的复用。
  • 别在前端裸调 SDK 的 key:地图 key 限制 HTTP referer / 包名,不做限制的 key 被盗刷,账单是你的。服务端代理转发 + 缓存是更干净的架构(下一节讲)。

地理编码:地址与坐标的双向翻译,缓存是省钱的关键

用户输入"朝阳区三里屯",你要变成坐标才能做附近搜索;反过来,拿到 GPS 坐标要显示"北京市朝阳区",需要逆地理编码。两个方向都是调地图厂商 API,都是按次计费。

  • 正向编码(地址→坐标):在商家入驻/发布房源时做一次,结果存进 DB。地址不变,坐标不变——这是一次性成本。
  • 逆向编码(坐标→地址):贵在高频。外卖骑手每 5 秒上报一次坐标,每次都调逆编码,一天就是几十万次调用。
  • 缓存策略:geohash 6 位精度约 1.2km × 0.6km——把逆编码结果按 6 位 geohash 做 key 缓存 7 天,同一条街的请求全部命中。实测这类缓存命中率能到 80%+,直接把账单砍掉四分之三。服务端加一层 Redis 缓存,别让每次请求都穿透到地图厂商。
  • 失败降级:地理编码 API 挂了,附近搜索不能跟着挂。降级为"只显示距离数字,不显示地址文字",核心链路保住。

隐私与合规:位置是敏感个人信息,采集侧就要收敛

这是位置功能区别于其他所有功能的地方:《个人信息保护法》和 GDPR 都把精确位置列为敏感个人信息,意味着:

  • 单独同意 + 最小必要。位置权限不能藏在"一揽子协议"里;"附近咖啡馆"只需要城市级精度,就别要 GPS 精度的持续定位。iOS / Android 的权限文案要写人话:"我们需要你的位置来找附近的店",而不是"需要位置权限"。拒绝率和你文案的诚实度直接相关。
  • 模糊化存储:对外展示、日志、数据分析,统一用 geohash 5 位(约 5km 精度)或更粗。全精度坐标只在服务端内存里算距离,用完即弃,不落盘。这是被监管问起来时最有力的回答。
  • 保留期:实时位置(如骑手轨迹)原始点位保留 7~30 天后聚合为统计值;历史轨迹导出功能只给用户本人。参考《DSR 实战》,删除请求下来时,位置数据要在同一批处理里清掉。
  • 后台定位是红线:除非你是导航/跑步类应用,否则不要申请后台位置权限。应用商店审核对后台定位极严,一人团队别在这上面浪费一轮审核周期。

进阶:实时位置(外卖/打车场景)

如果你的项目需要"地图上看骑手在哪",复杂度上一个台阶:

  • 上报节流:客户端每 5 秒上报一次够了,1 秒一次是浪费电量和流量。静止时(GPS 位移 < 20 米)不上报,服务端用最后位置+时间戳推算"X 分钟前更新"。
  • 下发聚合:用户端地图上 500 个骑手不能画 500 个点。服务端按当前地图视口做网格聚合(quadkey / geohash 前缀 group by),只下发聚合点+数量。缩放级别变化时重新聚合。
  • 通道选型:用 SSE 就够了(服务端→客户端单向),别上 websocket——双向通信在这里没有增量,还多一堆连接管理。这和《实时功能实战》的结论一致:按需选型,不炫技。
  • 断线语义:骑手 App 被杀掉,服务端 30 秒无心跳就把点标灰。用户看到"骑手 2 分钟前在线"比看到一个静止不动的假点体验好得多。

成本账:地图 API 的三种计费,分别怎么省

  • 地图加载(底图瓦片):按千次加载计费。省法:列表页别嵌地图(用静态缩略图代替),详情页才加载完整地图;瓦片 CDN 缓存。
  • 地理编码 / 逆编码:按次计费。省法:上一节的 geohash 缓存 + 入库时一次编码。
  • 路线规划(导航距离):最贵。按次计费且单价高。省法:列表排序用直线距离(免费,自己算),只有用户点了"导航"才调一次路线规划;"3 公里内"这种筛选用直线距离完全够用。

一句话:把"免费的自己算"(直线距离、geohash)和"花钱的调 API"(逆编码、路线规划)分开,能自己算的一律自己算。这是位置功能成本优化的全部心法。

测试:三个必测的边界

  1. 跨 180° 经线:geohash 前缀在经线两侧完全不同,九宫格逻辑要覆盖。用户在太平洋岛国也能用你的 App,别只在北京测。
  2. 高纬度:geohash 网格在高纬度变形(经线收敛),距离计算用球面公式(ST_Distance geography)而非平面公式。
  3. Mock 坐标:开发环境用固定坐标矩阵(北京/上海/纽约/伦敦各一个点 + 边界点),别每次都真机跑出去。iOS 模拟器和 Android 模拟器都支持 Mock 定位,一行配置的事。

上线前 checklist:8 项

  1. 入库坐标统一 WGS84,展示层按 SDK 做 GCJ-02/BD-09 转换
  2. PostGIS GIST 索引已建,百万行附近查询 EXPLAIN 确认走索引
  3. 九宫格邻块逻辑覆盖网格边界场景
  4. 分页用距离游标,不用 offset
  5. 逆地理编码有 Redis 缓存(geohash 6 位 key),穿透率可观测
  6. 位置权限文案是人话,拒绝后有降级(手动选城市)
  7. 对外展示/日志统一用模糊化坐标,全精度不落盘
  8. 地图 key 做了 referer/包名限制,或走服务端代理

小结:位置功能是"空间索引 + 成本意识 + 隐私敬畏"

技术上,它就是空间索引(PostGIS/geohash)+ 两阶段查询;成本上,记住"能自己算的别调 API";合规上,位置是敏感个人信息,采集侧就要收敛。三件事都做到,位置功能就是你产品的加分项,而不是上线后的雷。

相关指南:《vibe 项目的站内搜索实战》(文本/向量检索)与本篇(空间检索)是检索的两翼;《vibe 项目的日历预约与时区实战》(时间维度)与本篇(空间维度)合起来,就是 O2O 类项目的完整坐标系。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

视频剪辑时间线特写照片:显示器上可见剪辑轨道、调色轮与时间码
指南
用户传了个 4K 视频,服务器直接爆了:vibe 项目的视频处理管线实战

视频上传完只是开始:转码、多码率、HLS 切片、异步队列,缺一不可才能流畅播放。这篇给你完整管线架构图、FFmpeg 命令模板、HLS 参数甜点、BullMQ 状态机设计、缩略图/字幕/审核三个并行分支,以及上线前必须算的三笔成本账。

部署上线性能优化后端工程
夜间高速公路立交桥航拍照片,灯光勾勒出复杂的车流路线,隐喻数据库连接的调度与池化
指南
FATAL: too many clients already:vibe 项目的数据库连接池实战

FATAL: too many clients already 不是慢查询、也不是选错了库,而是连接数耗尽。这篇讲池化算账公式、PgBouncer 三种模式决策、Supabase/Neon 自带 pooler 接法、Prisma 的 prepared statement 大坑,以及排错对照表。

PostgreSQL性能优化后端工程
实拍:数据中心机房内成排的服务器机柜,呼应数据库归档主题
指南
表撑到一亿行之前:vibe 项目的数据归档与冷热分离实战

当只增不减的表撑过内存,备份和查询会一起衰减。vibe 项目归档实战:四个动手信号、三种策略对照(原生分区+pg_partman vs 归档表 vs Parquet 冷导出)、幂等可重入的归档作业状态机、归档后查询路由、回查解冻,以及归档与备份、合规保留期的关系。

数据工作流数据库后端工程