小i约球诞生记02:做附近球友匹配时,LBS 能力怎么选
本文是「小i约球诞生记」系列文章之一。
小i约球是一个微信小程序,主要帮乒乓球、羽毛球、台球、网球爱好者找到附近合适的球友。本文从一个具体技术或产品问题切入,记录约球小程序从 0 到 1 过程中的设计取舍。
小程序:微信搜索「小i约球」
前言
做“附近球友”这个功能时,我一开始以为 LBS 很简单。
拿到用户经纬度,查附近的人,按距离排一下,就结束了。
但真正放到约球场景里,会发现 LBS 不是一个单点能力,而是一组能力的组合:
- 小程序怎么拿定位
- 用户怎么选择常用约球地点
- 数据库存什么坐标格式
- 附近查询交给谁做
- 地图怎么展示点位
- 列表和地图是不是同一套数据
- 距离、球类、场景筛选怎么组合
可选方案
做这个功能时,大概有几种方案。
方案一:只存文本地址
最简单的方式是只让用户填写地址,比如“海淀区西二旗附近”“某某羽毛球馆”。
优点是实现非常简单。
缺点也很明显:
- 不能准确算距离
- 不能做地图点位
- 不能按半径查询
- 地址文本不统一,后期很难清洗
- 同一个地点可能有很多种写法
这个方案只适合非常早期的人工运营,不适合真正做附近匹配。
方案二:前端拿经纬度,后端普通字段存储
第二种方式是用小程序拿到经纬度,然后存在普通字段里:
{,latitude: 39.9,,longitude: 116.3,}。
这个方案比纯文本好很多,至少可以自己算距离,也可以在地图上显示点位。
但如果后端只是普通字段,附近查询就比较麻烦。你要么全量拉出来自己算,要么自己做经纬度范围过滤,再二次计算距离。
用户量小的时候能跑,数据量稍微大一点就会很难受。
方案三:使用云开发数据库 Geo 能力
小i约球最后选择的是这个方案。
用户选点或定位后,普通地址信息继续保留,同时额外存一个数据库 GeoPoint:
const location = db.Geo.Point(longitude, latitude)。
这样数据库里同时有两类字段:
- `address`:用于展示地点名称、详细地址、原始经纬度
- `location`:用于数据库地理位置查询
这种设计的好处是比较均衡。
展示层可以拿 `address`,查询层可以拿 `location`。前端不用自己全量算距离,后端也不用额外接一个地图服务。
对一个微信小程序 + 云开发项目来说,这是最顺手的方案。
为什么没有一开始接外部地图服务
外部地图服务当然也能做 LBS,比如地点检索、路线规划、行政区划、POI、逆地址解析等。
但小i约球第一阶段没有把它作为核心依赖。
原因是第一阶段需要解决的问题并不复杂:
- 用户选择一个约球地点
- 系统存下坐标
- 查询附近球友
- 地图展示点位
这些能力小程序和云开发已经能覆盖。
如果一开始就接外部地图服务,会多出不少成本:
- key 管理
- 请求配额
- 域名配置
- 坐标系处理
- 接口封装
- 失败兜底
所以我的选择是:第一阶段尽量用微信生态已有能力,等需要更复杂的 POI、路线、行政区、地理围栏时,再考虑接入外部地图服务。
小程序端:定位和选点是两件事
这里是很容易混淆的地方。
小程序里至少有两个和位置相关的能力:
- `getLocation`
- `chooseLocation`
它们解决的问题不一样。
`getLocation` 解决的是“用户现在在哪里”。
比如首页打开时,可以拿当前定位作为默认中心点,用来展示附近球友。
`chooseLocation` 解决的是“用户想把约球名片放在哪里”。
这个位置不一定是当前定位。一个用户可能现在在公司,但想发布家附近的羽毛球约球名片;也可能人在家里,但想发布常去球馆的乒乓球名片。
所以在小i约球里,这两个能力不能混用:
- 首页默认定位:用 `getLocation`
- 发布约球名片:用 `chooseLocation`
这是产品场景决定的,不只是技术接口差异。
小程序权限:定位不是无条件可用
小程序定位能力还涉及权限配置。
在 `app.json` 里,需要声明相关用途,比如:
{,"permission": {,"scope.userLocation": {,"desc": "用于查找附近球友和球馆",},},,"requiredPrivateInfos": [,"chooseLocation",,"getLocation",],}。
这里有两个现实问题。
第一,用户可能拒绝授权。
所以首页不能把定位当成绝对前提。定位失败时,要么用历史位置,要么展示默认城市或默认数据,要么引导用户开启定位。
第二,不同定位能力的审核和精度不一样。
低精度定位获取门槛低,但偏差可能比较大。对资讯类应用影响不大,但对约球这种线下场景,偏差几公里就很明显。这里的重点是:定位能力不能只看“能不能拿到”,还要看精度是否符合线下见面的场景。
云开发 Geo 能力适合做什么
云开发数据库的 Geo 能力,最适合做“给定一个点,查附近的数据”。
在小i约球里,它主要用在三个场景。
1. 查附近球友名片
用户打开首页时,根据用户位置查附近的约球名片。
这里除了地理位置,还要叠加业务条件:
- 不看自己的名片
- 按球类筛选
- 过滤无效数据
- 只查普通约球名片
这说明真实查询不会只有 `geoNear`,一定是“地理条件 + 业务条件”的组合。
2. 发布名片时找潜在匹配对象
用户发布新名片后,也可以反向查询附近已有球友。
这和首页查询方向不同。
首页查询是:我在哪里,附近有什么。 发布匹配是:我发了一个新点,附近谁可能对它感兴趣。
同样是 Geo 查询,但产品含义不同。
3. 查附近本地群
本地群也可以看作一种地理对象。
一个群如果对应某个街道、球馆或区域,就可以给它存一个位置。用户查附近群时,也可以用 Geo 能力做半径查询。
这说明位置能力最好设计成底层能力,而不是只服务某一个页面。
小程序 map 组件能做什么
小程序自带 `map` 组件,已经能满足第一阶段需求。
最核心的几个能力是:
- 设置中心点 `longitude` / `latitude`
- 设置缩放级别 `scale`
- 传入 `markers`
- 监听 marker 点击
- 监听地图视野变化
小i约球首页地图大概就是这种结构:
<map,longitude="{{user.location.longitude || 116.397428}}",latitude="{{user.location.latitude || 39.90923}}",markers="{{allMarkers}}",bindmarkertap="onMarkerTap",bindregionchange="onRegionChange",scale="13",/>。
这已经能完成基础的附近球友地图:
- 用户当前位置作为中心点
- 附近球友作为 marker
- 点击 marker 进入详情
- 缩放地图时调整 marker 展示方式
对第一阶段来说,不需要自己接一套 Web 地图库。
map 组件的限制
不过小程序 `map` 组件也不是没有限制。
最典型的是 marker 展示。
普通 marker 只能传图标、位置、大小等属性。如果你想实现更复杂的效果,比如:
- 用用户头像作为 marker
- 不同球类不同图标
- 教练、陪练、球馆、群聊带不同角标
- 缩放级别高时显示角标,缩放级别低时隐藏角标
就需要自己做更多处理。
坐标系和精度问题
小程序里常见坐标类型是 `gcj02`。
如果你的数据全部来自微信小程序的定位和选点,内部保持一致就好。
真正容易出问题的是这些情况:
- 外部地图服务返回的是另一种坐标系
- 后台导入的数据来自不同来源
- 用户定位是低精度
- 地址文本和经纬度不一致
小i约球第一阶段尽量避免引入多套坐标来源,就是为了降低这类问题。
但即便如此,定位精度仍然会影响体验。低精度定位可能偏几公里,这对“附近球友”来说不是小问题。这个问题会在后面的 Location 接口踩坑里展开。
半径怎么选
附近查询一定要面对一个问题:多远算附近?
这个值不是纯技术问题。
对线下运动来说,半径和城市密度、运动类型、用户习惯都有关系。
比如:
- 乒乓球可能更依赖固定球馆
- 羽毛球对场馆和时间更敏感
- 台球用户可能愿意走远一点
- 一线城市直线 5km 和实际通勤 5km 不是一回事
第一阶段可以先用一个相对保守的范围,比如 10km。它不一定完美,但足够验证产品闭环。
后面更合理的方式,是把半径做成可配置:
- 用户自己选择活动范围
- 不同城市使用不同默认半径
- 不同球类使用不同默认半径
- 根据数据密度动态扩大或缩小范围
总结
LBS 选型不能脱离业务场景。
如果只是做一个地图展示,可能一个 `map` 组件就够了。 如果只是存一个地址,文本字段也能应付。 但如果要做“附近球友匹配”,就必须把定位、选点、Geo 存储、附近查询、地图点位和权限降级一起考虑。
小i约球第一阶段选择了微信小程序能力 + 云开发 Geo 能力 + 小程序 map 组件。
这套方案的核心价值不是最强,而是足够贴合早期产品:成本低、集成顺、验证快。
---
本文属于「小i约球诞生记」系列。
项目官网:https://iyueqiu.cn 微信小程序:搜索「小i约球」