如果你有过周末临时想做饭、却为了几样配菜跑了三家超市的经历,你应该能理解我为什么要做 FindMyGrocery。这个名字拆开看其实很直白——Find 是“找”,My 是“你的”,Grocery 是“杂货生鲜”,合起来的意思就是:帮你最快找到想要的杂货。它的核心定位是一款基于位置的智能杂货购物应用,打开 App 输入你想买的商品,它能告诉你附近哪家店有货、哪家店更便宜、路线怎么走最顺,甚至预计几点到店不用排长队。这篇文章不打算写官方式的项目说明书,而是把这个产品从想法到 MVP 拆开来讲,包括功能设计取舍、核心技术点、落地时踩过的坑。如果你正在做生活服务类 App,或者对“地图 + 本地数据 + 推荐”这类产品从 0 到 1 感兴趣,这篇应该能给你一些真实参考。
1. 整体设计思路与需求拆解
1.1 用户到底在为什么事情头疼
做任何产品之前,我都习惯先列“痛到必须解决”的场景,而不是先想功能。FindMyGrocery 最开始源自一次真实的糟糕体验——家里缺酱油和鸡蛋,我先后跑了家附近的两家连锁超市,第一家没货,第二家有货但排队排了二十分钟。回来后我在想,如果有个东西能让我打开就看到附近哪家有货、哪家不用排队,是不是就能少浪费这一小时。
把这类琐碎吐槽收敛成需求,其实就三条:
- 不知道哪里有货,导致白跑一趟;
- 不知道哪家便宜,导致买贵了;
- 不知道门店当前的营业状态和拥挤程度,导致时间白耗。
这三条对应到产品功能上,分别是实时库存查询、价格比较、门店状态展示。这里有个很重要的产品判断:第一版不能功能铺太开,我把范围死死锁在“帮用户找到一个有货且价优的店”,其他所有功能都排到二期以后。很多同类产品死掉,不是因为功能太少,而是因为哪个都想做,最后哪个都做不透。
1.2 为什么选杂货品类,又为什么走数据聚合路线
选杂货这个品类不是拍脑袋。杂货生鲜是消费频次最高的线下零售品类,用户一周至少会买一两次,而且决策很轻——就是“今天吃啥、缺啥、下楼买”。这种高频、轻决策的场景,最适合用来验证 LBS 类工具产品的可行性。相反,如果一开始做服装、数码这种低频高决策品类,用户根本想不起来打开 App,冷启动很难做起来。
市面上不是没有生鲜电商、商超到家 App,但它们解决的是“线上下单、送到家”,跟 FindMyGrocery 解决的根本不是同一类问题。FindMyGrocery 的价值在于聚合和导航:它不做配送,也不自己备货,而是把城市里现有的线下零售信息结构化。基于常见实践来看,这类产品的核心竞争力不是供应链,而是两个数据能力:一是门店商品数据能不能及时拿到,二是检索结果的排序能不能让用户真的满意。想清楚“不做什么”比“做什么”更重要。做线下数据聚合的人,很容易被商家拉着做电商代运营、做配送,这些诱惑我全部挡住了。
1.3 核心功能优先级排序
我用的判断方法很朴素:把每个候选功能放进“用户使用频率 × 影响大小 × 实现成本”三个维度里打分。第一版最终只留下四个功能:
- 附近门店搜索(按距离和商品关键词);
- 商品库存查询(有货 / 少量 / 无货);
- 价格对比(同一商品在不同店的售价);
- 购物清单联动地图(一键生成到店顺序)。
至于店铺会员积分、优惠券推送、社区 UGC 评价,一期统统不做。理由很简单,MVP 阶段最重要的是验证“用户愿不愿意为了知道哪里有货而打开这个 App”,其他都是后话。后来数据也证实了这个判断,早期留存用户里,使用频率最高的就是这个“查询—到店”闭环,而评论、积分这类功能几乎没人点。
2. 核心技术点解析与方案选型
2.1 地理检索:不只是“找附近的店”
FindMyGrocery 第一个绕不开的技术点是“附近门店搜索”。听起来简单,就是按经纬度算距离,但真做起来有讲究。我选型时对比了 GeoHash 和 PostGIS 两种方案,最终选了 PostGIS,原因是:GeoHash 对“矩形范围查询”很友好,但做“按半径圈选 + 按价格过滤 + 按距离排序”这类组合条件时,自己拼字符串前缀反而麻烦。用 PostGIS 的话,一个 SQL 就能搞定:
SELECT id, name, address, ST_Distance(geom, ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography) AS distance FROM stores WHERE store_type = 'grocery' AND ST_DWithin(geom, ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography, 5000) AND price_level IS NOT NULL ORDER BY distance LIMIT 20;这段 SQL 的意思是:以某个坐标为圆心,取半径 5 公里内的杂货店,按距离升序取前 20 家。ST_DWithin 天然走空间索引,5 公里内查询耗时基本在几十毫秒级别。真正的坑在数据入库前的坐标清洗:商家填的门店地址是文本,必须靠地理编码服务转成坐标,而中文地址经常出现“XX 路与 XX 路交叉口”“XX 大厦 1 层”这类非标准写法,直接转换很容易偏出几条街。所以我在入库前会加一层地址清洗脚本,先做行政区划匹配,再做关键词补偿,最后把明显不在城市范围内的坐标打回人工复核。
2.2 库存数据:实时同步的难点与折中
如果说定位是骨架,库存数据就是 FindMyGrocery 的血液。但这块是整个项目里最麻烦的环节,因为线下超市的库存不像电商那样天然有开放接口。基于常见实践,可落地的接入方式大致有三种。
第一种是接入连锁商超的开放 API。现在不少大型连锁有供应链数据合作,能拿到指定门店的 SKU 级库存,但接口限流严重,通常只能覆盖高频 SKU,不可能全量同步。第二种是电子价签或收银系统的数据合作,精度高但需要门店部署对应硬件,前期覆盖范围很难铺开。第三种是众包采集,用户到店后在 App 里点击“有货 / 无货”标记,再用算法平滑处理多用户反馈,精度一般但冷启动时最现实。
我的方案是“API 为主、众包兜底”:对头部连锁店走 API 定时同步,对中小门店开放店员和普通用户上报入口,后台再用投票权重法把冲突标记做聚合。所谓投票权重法,比如同一个商品 10 分钟内出现 5 条“有货”和 1 条“无货”,我会结合上报者的历史准确率加权,得出一个“高概率有货”的置信度。这套逻辑单独看并不复杂,但它决定了后续排序和状态展示的可靠性。
2.3 价格比较:让数据在同一坐标系里可比
价格对比的坑比想象中多。同一瓶酱油可能有“标价”和“会员价”两种;同一款酸奶,有“单瓶价”和“整排价”。如果直接比数值,用户会骂数据不准。我在这层做了一套标准化处理:每个 SKU 维护一个单位基准价,也就是元/100ml 或元/100g,展示时优先显示基准价,同时保留原包装价。
举个例子:
- A 店卖某洗发水 750ml 装 39.9 元,基准价 = 39.9 / 7.5 = 5.32 元/100ml;
- B 店卖同款 500ml 装 29.9 元,基准价 = 29.9 / 5 = 5.98 元/100ml。
这样用户一眼能看出哪家更划算。为了防止用户拿便利店和大型超市比价来抬杠,我还在排序规则里加入了 store_type 权重,同类型门店之间优先比价,跨类型比价只做参考展示。这套规则花了一天写出来,却把“比价”从噱头变成了可用功能。
2.4 购物清单到路线规划的联动逻辑
清单功能本身不难,但 FindMyGrocery 把它跟地图联动起来,就有意思了。用户在清单里勾选商品后,系统会做两件事:一是按商品匹配各门店库存,找出“能一次覆盖最多清单项”的门店组合;二是对选中的多个门店做路径排序,避免走回头路。
第一个能力听起来很像算法题里的集合覆盖问题,但实际场景门店数量不大,单次覆盖最多几十家,所以早期我用穷举就能算。门店多了以后,换成了贪心策略:先选覆盖商品数最多的店,再在剩余商品里重复选店,直到清单覆盖完或者用户手动停止。实测下来,贪心策略对 5 到 8 家门店规模的场景已经足够快,也足够解释了。第二个能力直接调用地图服务的多点路径规划接口,传入门店坐标序列,拿回一条优化过的顺序即可。真正需要自己处理的反而是“用户指定某家非去不可”这类硬约束,系统得手动把这家店往路线里插。
3. 实操过程与核心环节实现
3.1 冷启动阶段的数据从哪来
项目刚启动时,最怕的就是“App 里什么都没有”。我给自己定的目标是先覆盖一个中等城市的三个核心商圈,大概 50 家门店。这个阶段没什么高级手段,就是拼执行。
第一,人工采集头部连锁的门店基础数据,地址、电话、营业时间这些公开信息直接从官网和地图服务整理同步,没有捷径。第二,通过商超电子版促销海报抓取 SKU 和促销价。很多超市每周都会发电子海报,我写了个解析脚本把海报里的商品名、规格、价格提取出来入库。这个方法很笨,但能在一两周内积累几千个有效 SKU。第三,拉早期用户做上报。上架第一周我找了 20 个朋友当体验官,让他们去超市买东西时顺手标记“有货 / 无货”,两周回传了 300 多条标记,用来校准置信度足够了。
做数据活性还有一个很关键的动作:SKU 归一化。超市海报里“海天生抽 500ml”“海天味极鲜 500ml”可能是不同条码,但用户搜索“海天酱油”时两个都得能出来。所以我单独建了一张 SKU 别名表,把同义词、品牌词、规格词拆开维护,搜索时做同义词扩展。
3.2 后端接口设计与数据模型
后端我用了典型的单体服务架构,接口按业务领域拆分,没有一上来就上微服务。核心表结构大约五张:门店表 stores、SKU 表 skus、门店库存表 store_inventory、商品价格表 product_prices、用户上报表 user_reports。其中 store_inventory 是最高频的查询表,我做了 (store_id, sku_id) 联合索引,并且把库存状态用 0/1/2 枚举表示 无货 / 少量 / 有货,而不是存一个浮动的数字。数字看起来很精确,但小样本上报场景下反而容易误导,枚举再配合置信度字段,展示逻辑反而干净。
接口设计上我总结出一个原则:接口返回的数据一定要“帮客户端少算一次”。比如门店列表接口除了返回距离,还会把一个已经算好的 display_distance 字段一并返回,避免客户端重复计算和格式化;门店详情接口直接在服务端把营业状态 open / closed 算好再返回。少让客户端做判断,线上错误就少一半。
3.3 移动端:地图交互的取舍
移动端我选的是 Flutter 跨平台方案,一个原因是开发效率高,另一个原因是后续想上 Web 端能省点事。地图组件用的是高德地图 SDK,在 Flutter 里通过平台通道封装了一层。界面交互上我坚持一个原则:首屏只做一件事——输入商品名,看地图上的结果点。筛选、排序、历史记录全部收起。因为用户打开 App 的动机是“找东西”,任何多余的入口都会稀释这个动机。
地图上的门店气泡会显示三样信息:距离、“有货 / 无货”状态、相对最低价标识。这三个信息是经过真实用户访谈验证的,最初我在气泡上放了门店评分和评论数,结果用户根本不看。他们只关心“有没有、贵不贵、远不远”。评分和评论还是放进详情页更合适。
3.4 推荐排序怎么调才靠谱
“Smartest Way”这个定位,最终要落在排序逻辑上。我的经验是不要用单一指标排序,而是用混合打分:
score = 0.4 × 距离分 + 0.35 × 库存置信度分 + 0.25 × 价格分
距离分用指数衰减,5 公里外基本趋近于 0;价格分用固定区间映射,价格越低分越高;库存置信度分就是前面说的上报投票加权结果。这样用户看到的结果一般是“近且大概率有货”,而不是“最便宜但未必有货”。
这套权重不是拍脑袋定的。第一版我把价格权重调成 0.4,结果出现了大量用户点击后发现门店太远、直接流失的情况。调成现在的比例后,到店转化率明显上升。排序权重这种事,不同城市、不同品类都会有差异,需要有专门的实验埋点来持续调整。
4. 常见问题与排查技巧实录
4.1 坐标偏移导致“5 公里内找不到店”
我第一次全量导入门店数据后,发现不少用户反馈“明明旁边就有超市,App 里却没有”。排查下来是地址转坐标的精度问题:中文地址里“某路 100 号”被地理编码服务解析偏差了好几百米,导致 ST_DWithin 计算时门店被排除在 5 公里圈外。
解决思路分两步:先对门店坐标做人工抽样复核,抽样比例不低于 20%;再把地理编码服务换成带“地址补全”能力的那款,把短地址先补全成标准地址再转坐标。从那以后我养成了一个习惯:凡是依赖外部坐标数据的业务,入库前必须写一个坐标合理性校验脚本,判断经纬度是否落在对应行政区范围内,越界的直接进人工复核池。
4.2 库存状态“有货”却总是扑空
早期用户经常抱怨:App 上写着有货,到店一看是空货架。这个问题实际上很难 100% 避免,因为库存本来就是实时变化的,任何数据源都有滞后。我能做的是让“有货”这个状态更保守——置信度低于阈值的 SKU 一律不显示为“有货”,改显示“可能有货”。另外在详情页加了一个“更新时间”,让用户看到这条信息是 5 分钟前还是 3 小时前的。信息透明本身就是一种信任建设,这个策略上线后投诉率降了不少,因为用户不会再把“信息滞后”归因于“App 骗人”,而是理解为“情况在变化”。
4.3 商家不配合,数据源接不上
这是整个项目里最现实的问题。中小商家对“开放数据”这件事没有概念,比较有效的办法是用利益换合作——在 App 里给商家提供免费的“门店客流预估”和“商品热度榜”作为交换。实际操作中,我帮一家店做了周边竞争分析报告,老板直接同意提供价签数据给我做测试。数据合作这种事,讲技术不如算账:让对方看到“你能帮我带来什么”,比讲一百句愿景都管用。
4.4 空间查询性能慢
门店数据量到 10 万级之后,不带索引的空间查询会慢到不可接受。我做性能优化时做了三件事:第一,确认所有空间查询字段都建了 GIST 索引,过滤条件用 ST_DWithin 而不是 ST_Distance;第二,按城市维度做分区,查询时先按城市裁一次范围;第三,对热门查询结果做 5 分钟 Redis 缓存,比如“北京国贸周边 2 公里内门店列表”这种高频查询,直接走缓存,数据库压力能减少一大半。
4.5 问题速查表
| 问题现象 | 可能原因 | 排查思路 | 解决措施 |
|---|---|---|---|
| 门店搜不到 | 坐标偏移 / 地址编码失败 | 检查原始地址与坐标的行政区域是否匹配 | 地址补全 + 人工复核池 |
| 库存显示不准 | 数据滞后 / 上报冲突 | 看更新时间与置信度 | 状态保守化 + 显示更新时间 |
| 距离算错 | 坐标系混用 | 检查 WGS84 与 GCJ02 是否统一 | 全链路统一坐标系 |
| 比价不公平 | 规格不同 | 检查是否用基准价比较 | 单位基准价 + 类型权重 |
| 列表加载慢 | 无索引 / 缓存击穿 | 看慢查询日志 | GIST 索引 + Redis 缓存 |
4.6 三个从实战里得来的心得
地图类产品最容易被低估的是数据清洗,“脏数据进、脏数据出”,后面所有算法都会被带偏。所以别为了技术炫技引入重型组件,早期我一度想用实时流处理框架做库存同步,后来发现定时任务加消息队列就够用了。还有,所有展示给用户的信息都要有对应的数据来源和时间戳,否则出了问题连排查入口都没有。这几条是我被用户反复质疑数据准确性之后,才真正刻进脑子里的。
5. 后续扩展方向与商业化思考
FindMyGrocery 做到这个阶段,已经有能力往两个方向延伸。一个是智慧购物清单:结合用户历史购买记录做周期性提醒,比如“你家的洗衣液估计快用完了”,再把周边超市的线上优惠券导流进来形成闭环。另一个是面向商家的数据增值服务,把脱敏后的周边客群画像、商品热度趋势做成月度报告。这条路我很看好,因为商家确实需要这些数据,只是以前没有人用这种角度帮他们整理。
技术上也可以再往前走,比如引入图像识别做货架商品识别,用户拍一张货架照片就能自动识别缺货情况,省去手动上报的成本。我研究过可行性,难点在于货架环境复杂、商品外观相似,准确率短期很难达到商用级别,所以大概率会先用众包标记来过渡。
最后再分享一点个人体会:像 FindMyGrocery 这种偏线下数据的产品,最怕的不是技术难度,而是对线下世界复杂性的低估。你永远想不到一个门店的营业时间会随着节假日怎么变,也想不到同一件商品在不同门店的条码可能不一样。我的做法是保持简单:小步快跑,每个版本只验证一个核心假设,把数据质量当成第一优先级来盯。做这类产品会有大量琐碎的数据维护工作,但每当你看到用户因为 App 少跑了一次冤枉路,又会觉得这些东西值回票价。如果你也打算做类似方向,建议先挑一个细分品类、一个城市扎下去,把一个闭环跑通,再谈扩张。