diff --git a/internal/web/fillup.go b/internal/web/fillup.go index 3e41aa2..e4844e0 100644 --- a/internal/web/fillup.go +++ b/internal/web/fillup.go @@ -206,6 +206,11 @@ func (s *Server) adminFillupPage(w http.ResponseWriter, r *http.Request) { s.serverError(w, r, err) return } + usedCats, err := s.usedCategories(ctx, ds.ID) + if err != nil { + s.serverError(w, r, err) + return + } total := len(rows) hinted := 0 for _, row := range rows { @@ -223,7 +228,7 @@ func (s *Server) adminFillupPage(w http.ResponseWriter, r *http.Request) { rows = rows[from:to] s.render(w, "admin/fillup", FillupData{ - Page: p, Rows: rows, Activities: acts, Categories: fillupCategories(), + Page: p, Rows: rows, Activities: acts, Categories: fillupCategories(usedCats), Total: total, WithHint: hinted, PageNo: pageNo, PageCount: pageCount, }) } @@ -255,8 +260,32 @@ type fillupCategory struct { Label string } -// fillupCategories 是类别下拉的选项:真机用过的 12 种在前,其余常见 MapKit 类别在后。 -func fillupCategories() []fillupCategory { +// usedCategories 列出这个数据集里真在用的类别(按地点数从多到少),排在静态表前面。 +// 候选得先跟已有数据对得上——否则用户会在这一页挑一个名字相近的静态枚举, +// 跟库里已有的值并存成两种,同一个意思分两处统计。 +func (s *Server) usedCategories(ctx context.Context, datasetID int64) ([]fillupCategory, error) { + rows, err := s.db.QueryContext(ctx, `SELECT poi_category, count(*) FROM places + WHERE dataset_id=$1 AND COALESCE(poi_category,'')<>'' GROUP BY 1 ORDER BY 2 DESC, 1`, datasetID) + if err != nil { + return nil, err + } + defer rows.Close() + var out []fillupCategory + for rows.Next() { + var v string + var n int + if err := rows.Scan(&v, &n); err != nil { + return nil, err + } + // 值可能是 MKPOICategory 枚举,也可能是真机自己写的中文——poiCategoryLabel 两种都认 + out = append(out, fillupCategory{v, fmt.Sprintf("%s(在用 %d)", poiCategoryLabel(v), n)}) + } + return out, rows.Err() +} + +// fillupCategories 是类别下拉的选项:数据集里真在用的排最前面,再跟真机用过的 12 种、 +// 其余常见 MapKit 类别。同一个值只出现一次——在用的那份信息更准,就顶掉静态表里的说明。 +func fillupCategories(inUse []fillupCategory) []fillupCategory { known := []string{ "MKPOICategoryRestaurant", "MKPOICategoryStore", "MKPOICategoryPublicTransport", "MKPOICategoryHotel", "MKPOICategoryLandmark", "MKPOICategoryFoodMarket", @@ -271,12 +300,23 @@ func fillupCategories() []fillupCategory { "MKPOICategoryNationalPark", "MKPOICategoryAquarium", "MKPOICategoryAmusementPark", "MKPOICategoryGasStation", "MKPOICategoryCarRental", "MKPOICategoryMarina", } - out := make([]fillupCategory, 0, len(known)+len(extra)) + seen := make(map[string]bool, len(inUse)+len(known)+len(extra)) + out := make([]fillupCategory, 0, len(inUse)+len(known)+len(extra)) + push := func(c fillupCategory) { + if c.Value == "" || seen[c.Value] { + return + } + seen[c.Value] = true + out = append(out, c) + } + for _, c := range inUse { + push(c) + } for _, c := range known { - out = append(out, fillupCategory{c, poiName(c) + "(真机用过)"}) + push(fillupCategory{c, poiName(c) + "(真机用过)"}) } for _, c := range extra { - out = append(out, fillupCategory{c, poiName(c)}) + push(fillupCategory{c, poiName(c)}) } return out } diff --git a/internal/web/fillup_test.go b/internal/web/fillup_test.go index a9bfc9f..69cc9f4 100644 --- a/internal/web/fillup_test.go +++ b/internal/web/fillup_test.go @@ -322,3 +322,57 @@ func TestFillupKeepsHandwrittenCandidates(t *testing.T) { } } } + +// 补全页的类别候选要「数据里在用的 ∪ 静态表」:库里真在用的值(尤其是真机自己写的中文) +// 原来在这一页根本选不到,只能手打,跟静态枚举并存成两种,同一个意思分两处统计。 +func TestFillupCategoriesIncludeUsedOnes(t *testing.T) { + f := newMovementFixture(t) + fillupPlace(t, f) // 补全页得有行才会渲染出下拉 + for _, p := range []struct { + pk int + cat string + }{ + {691001, "测试类别戊"}, {691002, "测试类别戊"}, {691003, "MKPOICategoryRestaurant"}, + } { + f.mustExec(t, `INSERT INTO places (dataset_id, src_pk, name, lat, lon, poi_category) + VALUES ($1,$2,'类别候选测试点',30,120,$3)`, f.dsID, p.pk, p.cat) + } + t.Cleanup(func() { + f.pg.ExecContext(f.ctx, `DELETE FROM places WHERE dataset_id=$1 AND src_pk BETWEEN 691001 AND 691003`, f.dsID) + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/admin/fillup", nil).WithContext(f.ctx) + f.srv.adminFillupPage(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("渲染失败: %d", rec.Code) + } + body := rec.Body.String() + + // 下拉每行渲染一份,要数重复就得只看其中一个 + seg := body[strings.Index(body, `