地点补全:按地名与坐标猜活动与类别,只填空

后台新增「地点补全」页,把「名下到访没设活动」和「类别为空」的地点列出来,
给出建议、由站长确认后再写。判断分三层:

- 本地关键词表:零延迟、离线,命中即高置信(「星巴克咖啡」→ 奶茶和甜甜水)
- 高德周边搜索:按坐标问「这里是什么 POI」,比按名字搜准;
  拿到的类型再映射成活动与 MapKit 类别
- 人工:都猜不出的留空,站长自己填;填了列表里没有的名字会自动建成新活动类型

两条底线:**只填空**(已有活动或类别的一律不动);**活动按地点批量设**,
一次把它名下所有没设活动的到访一起填上,否则同一个地点会在统计里分裂成两种活动。

类别写的是 Apple MapKit 的枚举名(真机确认用过 12 种),不是高德分类。

规则上踩过两个坑,都写进注释了:匹配前要先掐掉括号里的地址(否则
「尚优居客栈(大理古城步行街店)」会命中「步行街」),并且按词长优先
(否则「马鞍山公园」会命中「山」);单字关键词一律不要,误命中率太高。
This commit is contained in:
tamakyi
2026-09-19 16:31:01 +08:00
parent 30c502ce7b
commit 6c061a0548
8 changed files with 1012 additions and 3 deletions
+11 -3
View File
@@ -351,6 +351,13 @@ func (s *Server) apiRegeo(w http.ResponseWriter, r *http.Request) {
// 半径给 200 米:再大容易匹配到马路边上不相干的店;取不到就返回 ok=false,
// 界面上只显示地址。
func (s *Server) amapNearestPOI(ctx context.Context, lat, lon float64) (name string, dist float64, ok bool) {
name, _, dist, ok = s.amapAround(ctx, lat, lon)
return name, dist, ok
}
// amapAround 周边搜索:拿最近那个 POI 的名字、类型与距离。
// typ 是高德的分类串(形如「餐饮服务;咖啡厅」),地点补全靠它猜活动与类别。
func (s *Server) amapAround(ctx context.Context, lat, lon float64) (name, typ string, dist float64, ok bool) {
v := url.Values{}
v.Set("key", s.cfg.GeocodeKey)
v.Set("location", fmt.Sprintf("%.6f,%.6f", lon, lat))
@@ -359,20 +366,21 @@ func (s *Server) amapNearestPOI(ctx context.Context, lat, lon float64) (name str
v.Set("sortrule", "distance") // 按距离排序,第一条就是最近的
body, err := s.amapGet(ctx, aroundURL+"?"+v.Encode())
if err != nil {
return "", 0, false
return "", "", 0, false
}
var raw struct {
Status string `json:"status"`
Pois []struct {
Name string `json:"name"`
Type string `json:"type"`
Distance string `json:"distance"`
} `json:"pois"`
}
if err := json.Unmarshal(body, &raw); err != nil || raw.Status != "1" || len(raw.Pois) == 0 {
return "", 0, false
return "", "", 0, false
}
d, _ := strconv.ParseFloat(raw.Pois[0].Distance, 64)
return raw.Pois[0].Name, d, true
return raw.Pois[0].Name, raw.Pois[0].Type, d, true
}
// amapRegeo 调一次高德的逆地理编码。失败返回 ok=false,调用方留空即可。