Files
rond-with-you/internal/web/fillup_rules_test.go
tamakyi 6c061a0548 地点补全:按地名与坐标猜活动与类别,只填空
后台新增「地点补全」页,把「名下到访没设活动」和「类别为空」的地点列出来,
给出建议、由站长确认后再写。判断分三层:

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

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

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

规则上踩过两个坑,都写进注释了:匹配前要先掐掉括号里的地址(否则
「尚优居客栈(大理古城步行街店)」会命中「步行街」),并且按词长优先
(否则「马鞍山公园」会命中「山」);单字关键词一律不要,误命中率太高。
2026-09-19 16:31:01 +08:00

109 lines
4.0 KiB
Go

package web
import "testing"
// 用例直接用站点里真实存在的地名,规则改坏了这里立刻会响。
func TestGuessActivityByName(t *testing.T) {
cases := []struct{ name, want string }{
{"星巴克咖啡", "奶茶和甜甜水"},
{"后谷咖啡", "奶茶和甜甜水"},
{"花溪十里河滩绿道", "散步"},
{"马鞍山公园", "散步"},
{"河池市体育馆", "户外活动"},
{"喜洲古镇洱海环境监测站", "旅游景点"},
{"十里河滩旅游度假区-孔子圣像", "旅游景点"},
{"尚优居客栈(大理古城步行街店)", "酒店"}, // 括号里是地址,主体才是客栈
{"鑫圣餐厅", "餐厅"},
{"彭大花·白族小花园餐厅(广武路店)", "餐厅"},
{"大昌盛超市", "小商店"},
{"中国福利彩票", "彩票购买点"},
// 猜不出来的必须留空,交给高德或者人工——宁可空着也别瞎填
{"蜜雪冰城", ""},
{"昆明站", ""}, // 单字「站」误命中太多,交给高德
{"独山路28-17号", ""}, // 路名里有「山」但不是山
{"银滩镇", ""},
{"傣思蜜清真手抓饭", ""},
{"何孃孃小锅米线", ""},
{"", ""},
}
for _, c := range cases {
if got := guessActivityByName(c.name); got != c.want {
t.Errorf("guessActivityByName(%q) = %q,期望 %q", c.name, got, c.want)
}
}
}
func TestGuessCategoryFromAmapType(t *testing.T) {
cases := []struct{ typ, want string }{
{"餐饮服务;咖啡厅", "MKPOICategoryCafe"},
{"餐饮服务;中餐厅", "MKPOICategoryRestaurant"},
{"餐饮服务;快餐厅", "MKPOICategoryRestaurant"},
{"购物服务;购物中心", "MKPOICategoryStore"},
{"购物服务;便利店", "MKPOICategoryStore"},
// 小类优先:「风景名胜」是大类,这地方其实是个公园
{"风景名胜;公园", "MKPOICategoryPark"},
{"住宿服务;宾馆酒店", "MKPOICategoryHotel"},
{"交通设施服务;地铁站", "MKPOICategoryPublicTransport"},
{"交通设施服务;机场", "MKPOICategoryAirport"},
{"体育休闲服务;运动场馆", "MKPOICategoryStadium"},
{"医疗保健服务;综合医院", "MKPOICategoryHospital"},
// 大类命中、小类不认识时退回大类
{"购物服务;从未见过的子类", "MKPOICategoryStore"},
{"", ""},
{"未知;未知", ""},
}
for _, c := range cases {
if got := guessCategoryFromAmapType(c.typ); got != c.want {
t.Errorf("guessCategoryFromAmapType(%q) = %q,期望 %q", c.typ, got, c.want)
}
}
}
func TestGuessActivityFromAmapType(t *testing.T) {
cases := []struct{ typ, want string }{
{"餐饮服务;咖啡厅", "奶茶和甜甜水"},
{"餐饮服务;小吃快餐", "美食街"},
{"购物服务;便利店", "小商店"},
{"住宿服务;宾馆酒店", "酒店"},
{"风景名胜;公园", "散步"},
{"体育休闲服务;运动场馆", "户外活动"},
{"未知;未知", ""},
}
for _, c := range cases {
if got := guessActivityFromAmapType(c.typ); got != c.want {
t.Errorf("guessActivityFromAmapType(%q) = %q,期望 %q", c.typ, got, c.want)
}
}
}
// 类别值必须是真机能认的枚举名,别写出别的形状。
func TestKnownMKCategoriesShape(t *testing.T) {
for c := range knownMKCategories {
if len(c) <= len("MKPOICategory") || c[:13] != "MKPOICategory" {
t.Errorf("类别 %q 不符合 MKPOICategory<名字> 的形状", c)
}
}
if !mkCategoryKnown("MKPOICategoryRestaurant") {
t.Error("真机用过的类别应被认得")
}
if mkCategoryKnown("MKPOICategoryCafe") {
t.Error("这一次新猜的类别不该被算作「真机用过」")
}
}
func TestTrimParens(t *testing.T) {
cases := []struct{ in, want string }{
{"尚优居客栈(大理古城步行街店)", "尚优居客栈"},
{"三哥·思茅菜(北区店)", "三哥·思茅菜"},
{"没有括号的名字", "没有括号的名字"},
{"开头就是括号的", "开头就是括号的"}, // 整串都是括号开头时不截断,避免截成空串
}
for _, c := range cases {
if got := trimParens(c.in); got != c.want {
t.Errorf("trimParens(%q) = %q,期望 %q", c.in, got, c.want)
}
}
}