跳转到内容

Proximity Service — 附近服务

公司搬到新办公区,周围吃饭的地方大家都不熟。同事们每天中午都在群里问”附近有什么好吃的”。你决定做一个小工具,把附近的餐厅整理好,打开就能看到距离自己最近的餐厅,按距离排序,还能按菜系筛选。餐厅就那么几十家,手动整理一下数据就行。

用浏览器定位 API 和 Haversine 公式,实现纯前端的附近餐厅查找。

用纯前端实现一个附近餐厅查找工具,要求:
1. 餐厅数据硬编码为 JSON 数组,包含 50 家餐厅
2. 每个餐厅字段:id, name, category(川菜/粤菜/日料/西餐/快餐/咖啡), rating(1-5), lat, lng, address, avgPrice
3. 使用 navigator.geolocation 获取用户当前位置
4. 如果用户拒绝定位,提供手动输入经纬度的选项(默认填入一个示例坐标)
5. 用 Haversine 公式计算每家餐厅到用户的距离
6. 默认按距离从近到远排序
7. 支持排序切换:距离优先 / 评分优先 / 价格从低到高
8. 支持筛选:按菜系分类、按距离范围(500m/1km/3km)、按评分(4分以上)
9. 餐厅卡片显示:名称、菜系标签、距离、评分(星星)、人均价格
10. 页面顶部显示当前定位状态和坐标
11. 单个 HTML 文件,内联 CSS 和 JS
输出一个可以直接打开的 HTML 文件。
  • 页面加载后请求定位权限
  • 定位成功后显示坐标和餐厅列表
  • 拒绝定位后手动输入坐标功能正常
  • 距离计算结果合理(公里级别)
  • 排序切换功能生效
  • 菜系筛选和距离范围筛选正常
  • 多个筛选条件可以叠加
  • 餐厅卡片信息完整
  • 浏览器 Geolocation API 的使用和权限处理 → M1
  • Haversine 公式计算球面距离 → M9
  • 前端排序和多条件筛选的实现 → M2
  • 地理位置数据的基本概念(经度、纬度) → M2

V2「餐厅老板们想自己维护信息,50 家不够用了」

Section titled “V2「餐厅老板们想自己维护信息,50 家不够用了」”

附近餐厅工具在公司群里传开了,周围的餐厅老板听说后也想加入。但是每次新增餐厅都要改代码里的 JSON,太麻烦了。而且不光是餐厅,楼下便利店、药房、快递站也想加进来。你需要一个后端,让商户自己注册和维护信息,用户打开页面实时看到最新的商户列表。

搭建商户管理后端,实现动态数据的 CRUD 和距离计算。

用 Go + Gin + SQLite 实现一个附近商户服务,要求:
1. 数据模型:Business(id, name, category, lat, lng, address, phone, avg_price, rating, description, status, created_at, updated_at)
2. 商户 CRUD API:
- POST /api/businesses — 注册商户
- GET /api/businesses/:id — 商户详情
- PUT /api/businesses/:id — 更新商户信息
- DELETE /api/businesses/:id — 注销商户(软删除)
3. 附近搜索 API:
- GET /api/nearby?lat=31.23&lng=121.47&radius=3000&category=餐厅&sort=distance&page=1&size=20
- 在 Go 代码中用 Haversine 公式计算距离
- 先用经纬度范围粗筛(矩形框过滤,减少计算量)
- 再精确计算距离并排序
- 返回结果包含计算出的 distance 字段(单位:米)
4. 分类 API:GET /api/categories — 返回所有分类和每个分类的商户数量
5. 商户数据校验:
- name 必填,长度 2-50
- lat 范围 -90 到 90,lng 范围 -180 到 180
- category 必须是预定义的枚举值
6. 准备种子数据:100 家示例商户(分布在一个真实城市坐标附近)
7. 前端页面:
- 附近搜索页面(列表 + 筛选 + 排序)
- 商户注册表单
- 商户详情页
输出完整项目结构和代码。
  • 商户注册、查询、更新、删除功能正常
  • 数据校验生效(无效经纬度被拒绝)
  • 附近搜索返回正确的距离排序结果
  • radius 参数限制范围有效
  • 分页参数正常工作
  • 分类筛选和排序正常
  • 软删除的商户不出现在搜索结果中
  • 种子数据加载成功
  • 应用层距离计算的实现和优化(矩形预筛 + Haversine 精算) → M9
  • 地理数据的校验规则 → M2
  • 软删除模式(status 字段 vs 物理删除) → M2
  • 枚举值校验策略 → M1
  • 种子数据设计技巧 → M2
  • 从静态数据到动态 CRUD 的架构演进 → M8

V3「有 5 万家商户了,每次搜索要 3 秒」

Section titled “V3「有 5 万家商户了,每次搜索要 3 秒」”

服务越做越大,接入了 5 万家商户。用户搜”附近 1 公里内的餐厅”,后端要对 5 万条数据逐条算 Haversine 距离,然后排序取前 20 条,一次查询要 3 秒。高峰期(午饭前 11:30)并发 500 请求,服务器 CPU 直接打满。而且热门区域(如商业中心方圆 1 公里)被反复查询,每次都重新计算完全一样的结果。

用空间索引和缓存消除逐行扫描瓶颈,将搜索延迟从秒级降到毫秒级。

用 Go + Gin + PostgreSQL + PostGIS + Redis 实现一个高性能附近商户搜索服务,要求:
1. 数据库设计:
- Business 表增加 location 字段(PostGIS geometry(Point, 4326) 类型)
- 创建 GiST 空间索引
- 写入商户时自动从 lat/lng 生成 geometry:ST_SetSRID(ST_MakePoint(lng, lat), 4326)
2. 空间查询 API:
- GET /api/nearby?lat=31.23&lng=121.47&radius=3000&category=&sort=distance&page=1&size=20
- 使用 ST_DWithin(location, ST_MakePoint(lng, lat)::geography, radius) 做范围查询
- 使用 ST_Distance(location, ST_MakePoint(lng, lat)::geography) 计算精确距离
- 支持按距离、评分、价格排序
3. Redis 缓存策略:
- 将地图按 geohash 划分为网格(精度 6,约 1.2km x 0.6km)
- 缓存 key:nearby:{geohash}:{category}:{sort}
- 缓存过期时间 5 分钟
- 商户信息更新时清除相关 geohash 网格的缓存
4. 性能优化:
- 数据库连接池配置(最大 20 连接)
- SQL 查询分析:EXPLAIN ANALYZE 验证走空间索引
- 搜索结果返回查询耗时
5. 管理接口:
- POST /api/admin/cache/clear — 手动清除所有缓存
- GET /api/admin/stats — 缓存命中率统计
6. 种子数据:生成 50000 条模拟商户数据(脚本生成,均匀分布在指定城市范围)
7. 压测脚本:用 Go 写一个并发测试脚本,模拟 100 并发搜索请求
输出完整项目结构、Docker Compose(PostgreSQL + PostGIS + Redis)、数据库迁移 SQL、代码。
  • PostGIS 空间索引创建成功
  • ST_DWithin 查询使用 GiST 索引(EXPLAIN ANALYZE 验证)
  • 5 万数据量下单次查询 < 50ms
  • Redis 缓存命中时查询 < 5ms
  • 缓存未命中时查询结果正确且被缓存
  • 商户更新后相关缓存被清除
  • 分类筛选和排序使用空间索引
  • 压测 100 并发下平均响应时间 < 100ms
  • 缓存命中率统计准确
  • PostGIS 空间数据类型和空间索引(GiST) → M2
  • ST_DWithin 和 ST_Distance 空间查询函数 → M10
  • GeoHash 编码原理和网格化缓存策略 → M4
  • Redis 缓存设计:key 命名、过期策略、缓存失效 → M4
  • 数据库连接池配置和调优 → M8
  • EXPLAIN ANALYZE 查询分析 → M2
  • 从应用层计算到数据库空间索引的性能飞跃 → M8
  • 热点数据缓存的通用模式 → M4

V4「商户要实时更新营业状态和菜单」

Section titled “V4「商户要实时更新营业状态和菜单」”

商户数量达到 1000 家,商户老板们有了新需求:希望能实时更新营业状态(营业中/休息中/临时关闭),修改菜单和价格,设置特殊营业时间(节假日调整)。用户也反馈搜到的餐厅经常是”关门状态”,白跑一趟。现有系统只有基础的商户信息,缺少动态运营数据的管理能力,缓存也导致状态更新不及时。

实现商户实时状态更新、菜单管理 CRUD、缓存主动失效策略,以及基于营业时间的智能过滤逻辑。

在现有附近商户搜索系统基础上,增加商户运营管理功能,要求:
1. 实时状态更新:
- 商户状态枚举:open(营业中)、closed(已打烊)、temporarily_closed(临时关闭)、busy(繁忙)
- PUT /api/businesses/:id/status — 更新营业状态
- 状态变更时立即清除该商户相关的所有缓存(geohash 网格缓存 + 商户详情缓存)
- 搜索结果默认只返回 open 和 busy 状态的商户(可选参数 include_closed=true 显示全部)
- 商户状态变更记录日志(用于分析营业规律)
2. 菜单管理:
- 数据模型:Menu(id, business_id, name, description, price, category, image_url, is_available, sort_order)
- CRUD API:
- POST /api/businesses/:id/menu — 添加菜品
- GET /api/businesses/:id/menu — 获取菜单列表(按 category 分组)
- PUT /api/menu/:id — 更新菜品信息
- DELETE /api/menu/:id — 删除菜品
- PUT /api/menu/:id/availability — 切换菜品是否可用(今日售罄)
- 搜索 API 支持按菜品名搜索商户:GET /api/search/menu?q=小龙虾
3. 营业时间管理:
- 数据模型:BusinessHours(id, business_id, day_of_week, open_time, close_time, is_closed)
- 支持每天不同的营业时间
- 特殊日期覆盖:SpecialHours(id, business_id, date, open_time, close_time, is_closed, note)
- 搜索时自动判断当前是否在营业时间内
- GET /api/businesses/:id/hours — 获取一周营业时间和近期特殊日期
4. 缓存失效策略优化:
- 状态更新走主动失效(而非等过期)
- 菜单更新只失效商户详情缓存(不影响列表缓存)
- 营业时间判断结果缓存 1 分钟(避免每次查询都计算)
- 缓存预热:每天凌晨预计算热门区域的搜索缓存
输出增量数据模型、API 代码、缓存策略和营业时间计算逻辑。
  • 商户状态更新后搜索结果立即反映(不等缓存过期)
  • 默认搜索不显示已关闭商户
  • 菜品 CRUD 功能完整
  • 按菜品名搜索商户功能正常
  • 营业时间设置和自动判断正确
  • 特殊日期营业时间覆盖普通设置
  • 缓存主动失效策略生效
  • 缓存预热任务正常执行
  • 缓存主动失效 vs 被动过期策略 → M4
  • 状态机设计和状态流转管理 → M2
  • 营业时间的时间逻辑处理(时区、跨天、特殊日期) → M9
  • 多级缓存失效粒度控制 → M4
  • 菜单搜索和关联查询设计 → M10
  • 缓存预热策略减少冷启动延迟 → M4

V5「用户要看评价和评分,还要按评分排序」

Section titled “V5「用户要看评价和评分,还要按评分排序」”

用户量增长到 1 万,大家不再满足于只看距离和菜单,希望能看到其他人的评价和评分,按评分排序选餐厅。商户也希望能回复用户评价,提升口碑。但评价数据量大(平均每个商户 50 条评价),简单地把评分加到搜索排序里会让新商户永远排不到前面。需要一套综合评分算法,平衡距离、评分和时效性。

搭建评价系统,设计综合评分算法(距离+评分+时效性),实现搜索结果多样性,预计算热门区域排行榜。

在现有附近商户搜索系统基础上,增加评价系统和智能排序,要求:
1. 评价系统:
- 数据模型:Review(id, business_id, user_id, rating(1-5), content, images, reply, reply_at, created_at)
- API:
- POST /api/businesses/:id/reviews — 发表评价(一个用户对一个商户只能评价一次)
- GET /api/businesses/:id/reviews — 获取评价列表(分页,支持按时间/评分排序)
- PUT /api/reviews/:id/reply — 商户回复评价
- GET /api/businesses/:id/rating-summary — 评分概要(平均分、各星级占比、总评价数)
- 评分更新后重新计算商户平均评分(异步更新,避免阻塞)
- 防刷策略:同一用户每天最多评价 10 个商户
2. 综合评分算法(Composite Score):
- score = w1 × distance_score + w2 × rating_score + w3 × recency_score
- distance_score:距离越近分越高(1km 内满分,3km 衰减到 0.5)
- rating_score:贝叶斯平均评分(避免评价少的商户评分极端)
- recency_score:最近 30 天有新评价的商户加分
- 权重可配置:默认 w1=0.4, w2=0.4, w3=0.2
- 搜索 API 新增排序选项:sort=composite
3. 搜索结果多样性(Diversification):
- 避免搜索结果前 10 条全是同一分类(如全是快餐)
- 多样性算法:每个分类最多连续出现 3 个,然后插入其他分类
- 可通过参数关闭多样性:diverse=false
4. 热门区域预计算:
- 划分城市热门商圈(如 geohash 精度 5 的网格)
- 定时任务每小时预计算每个商圈的 TOP 20 商户排行榜
- 搜索时优先查预计算结果,减少实时计算压力
- GET /api/hotspots — 返回热门商圈列表和各商圈 TOP 3 商户
输出评价系统代码、评分算法、多样性排序和预计算任务。
  • 评价发表、列表、回复功能正常
  • 同一用户不能重复评价同一商户
  • 防刷限制生效(每天 10 条上限)
  • 贝叶斯平均评分计算正确(新商户不会因 1 条五星评价排第一)
  • 综合评分排序结果合理(近+好评优先)
  • 多样性排序避免同分类扎堆
  • 热门商圈预计算结果正确
  • 预计算缓存有效,搜索延迟下降
  • 评价系统和防刷策略设计 → M2
  • 贝叶斯平均评分避免小样本偏差 → M9
  • 综合评分算法(多因子加权) → M9
  • 搜索结果多样性算法 → M10
  • 预计算和物化视图加速查询 → M4
  • 异步评分更新避免写入瓶颈 → M5
  • 从单维排序到多因子综合排序的演进 → M8

V6「覆盖全国,不同城市的数据要就近查询」

Section titled “V6「覆盖全国,不同城市的数据要就近查询」”

服务覆盖全国,商户超过 10 万家,用户遍布数百个城市。所有数据都在一个 PostgreSQL 实例里,单表行数过千万,空间查询性能开始下降。不同城市的用户查询完全独立(上海用户不会搜北京的商户),但数据却混在一起。另外运营想做区域化推广活动(如某城市发优惠券),需要地理围栏功能。高峰期全国并发请求过万,单数据库实例已经扛不住了。

按城市/区域分片数据库,多区域部署服务实例,实现基于位置的请求路由,支持地理围栏促销活动,搭建实时需求热力图。

将附近商户搜索服务升级为全国化分布式架构,要求:
1. 数据库按城市/区域分片:
- 分片策略:按城市编码(city_code)分片,每个城市一个逻辑分片
- 中小城市合并到区域分片(如华东、华南、华北、西南等)
- 分片路由表:city_code → shard_id 映射,存储在 Redis
- 跨城市查询:用户在城市边界时查询两个分片并合并结果
- 新城市上线自动创建分片(基于模板)
2. 多区域部署:
- 华东(上海)、华南(广州)、华北(北京)三个区域各部署服务实例
- 用户请求根据 IP 地理位置路由到最近的区域
- DNS 级别的地理路由(GeoDNS)或应用层路由
- 区域间数据独立,全国商户目录通过中心同步
3. 基于位置的请求路由:
- 请求到达后,根据查询坐标确定所属城市
- 路由到对应城市的数据分片查询
- 城市边界 5km 缓冲区内的请求同时查询相邻城市分片
- 路由决策缓存(同一用户短时间内的请求路由到同一分片)
4. 地理围栏(Geofencing)促销:
- 数据模型:Promotion(id, name, polygon(地理围栏多边形), discount, start_time, end_time)
- 用户进入围栏区域时推送促销信息
- POST /api/admin/promotions — 创建促销活动(画围栏区域)
- GET /api/promotions/nearby?lat=&lng= — 查询用户所在围栏内的促销
- PostGIS ST_Contains 判断点是否在多边形内
5. 实时需求热力图:
- 记录每次搜索的坐标和时间
- 实时聚合为热力图数据:按 geohash 精度 6 的网格统计搜索次数
- GET /api/admin/heatmap?timerange=1h — 返回最近 1 小时的需求热力图
- 热力图数据用于指导商户拓展(哪些区域需求多但商户少)
- 使用 Redis HyperLogLog 统计独立用户数
6. 全国监控大盘:
- 各区域 QPS、延迟、错误率
- 各城市商户数量和活跃度
- 分片健康状态和数据量
- 跨区域查询比例和延迟
输出分片方案、路由逻辑、地理围栏代码、热力图聚合和监控大盘设计。
  • 按城市分片后查询只命中对应分片
  • 城市边界缓冲区查询合并两个分片结果
  • 用户请求路由到最近的区域服务
  • 新城市上线自动创建分片
  • 地理围栏创建和判断功能正常
  • 用户位于围栏内时收到促销信息
  • 需求热力图数据实时聚合
  • HyperLogLog 独立用户统计准确
  • 全国监控大盘指标正常采集
  • 分片故障时降级方案生效
  • 数据库分片策略(按地理区域分片) → M2
  • 分片路由和跨分片查询 → M8
  • GeoDNS 和地理位置路由 → M13
  • 地理围栏(Geofencing)和 ST_Contains 空间查询 → M10
  • 多区域部署和就近访问 → M17
  • 实时热力图聚合(geohash + 时间窗口) → M9
  • HyperLogLog 近似计数 → M4
  • 从单库到分片的水平扩展演进 → M8
  • 全国分布式系统的监控和可观测性 → M15