package web import ( "context" "database/sql" "testing" ) // 列表页(fillTopicStatsBatch:一条 UNION ALL)与详情页(topicContent:逐专题查询) // 对同一专题必须给出同样的统计 —— 列表上的数字点进去要对得上。 // 顺带钉住批量版的占位符编号:错一个就是 pgx「expected N arguments, got M」。 func TestTopicStatsBatchMatchesDetailPage(t *testing.T) { f := newLoginFixture(t) ctx := context.Background() var ds int64 if err := f.srv.db.QueryRowContext(ctx, `SELECT id FROM datasets WHERE is_active LIMIT 1`).Scan(&ds); err != nil { t.Skipf("没有生效数据集:%v", err) } var minAt, maxAt sql.NullTime if err := f.srv.db.QueryRowContext(ctx, `SELECT min(arrival), max(arrival) FROM visits WHERE dataset_id=$1`, ds).Scan(&minAt, &maxAt); err != nil { t.Fatal(err) } if !minAt.Valid || !maxAt.Valid { t.Skip("生效数据集里没有到访记录") } var lat, lon float64 if err := f.srv.db.QueryRowContext(ctx, `SELECT COALESCE(lat,0), COALESCE(lon,0) FROM places WHERE dataset_id=$1 ORDER BY visit_count DESC LIMIT 1`, ds).Scan(&lat, &lon); err != nil { t.Fatal(err) } var city string _ = f.srv.db.QueryRowContext(ctx, `SELECT city FROM places WHERE dataset_id=$1 AND COALESCE(city,'')<>'' GROUP BY city ORDER BY count(*) DESC LIMIT 1`, ds).Scan(&city) mid := minAt.Time.Add(maxAt.Time.Sub(minAt.Time) / 2) base := []Topic{ {ID: 1, Title: "全时段", StartAt: minAt.Time, EndAt: maxAt.Time}, {ID: 2, Title: "前半段", StartAt: minAt.Time, EndAt: mid}, {ID: 3, Title: "后半段", StartAt: mid, EndAt: maxAt.Time}, {ID: 4, Title: "按区域圈定", StartAt: minAt.Time, EndAt: maxAt.Time, CenterLat: lat, CenterLon: lon, RadiusKm: 30}, {ID: 5, Title: "区域圈不到任何地点", StartAt: minAt.Time, EndAt: maxAt.Time, CenterLat: 1, CenterLon: 1, RadiusKm: 1}, } type round struct { name string regions []string exclude bool wantNonZero bool } rounds := []round{{name: "无区域过滤", wantNonZero: true}} if city != "" { rounds = append(rounds, round{name: "白名单:" + city, regions: []string{city}, wantNonZero: true}, round{name: "黑名单:" + city, regions: []string{city}, exclude: true}, ) } for _, rd := range rounds { // 详情页路径:逐专题查询 want := append([]Topic(nil), base...) for i := range want { points, visits, days, cities, err := f.srv.topicContent(ctx, &want[i], ds, rd.regions, rd.exclude) if err != nil { t.Fatalf("[%s] topicContent #%d: %v", rd.name, want[i].ID, err) } want[i].PlaceCount, want[i].VisitCount = len(points), visits want[i].DayCount, want[i].CityCount = days, cities for _, p := range points { want[i].DwellMin += p.Dwell } } // 列表页路径:一条 UNION ALL got := append([]Topic(nil), base...) if err := f.srv.fillTopicStatsBatch(ctx, got, ds, rd.regions, rd.exclude); err != nil { t.Fatalf("[%s] fillTopicStatsBatch: %v", rd.name, err) } nonZero := 0 for i := range got { if got[i].PlaceCount != want[i].PlaceCount || got[i].VisitCount != want[i].VisitCount || got[i].DayCount != want[i].DayCount || got[i].CityCount != want[i].CityCount || got[i].DwellMin != want[i].DwellMin { t.Errorf("[%s] 专题 #%d 批量与详情页不一致:\n 批量 地点/到访/天数/城市/停留 = %d/%d/%d/%d/%d\n 详情页同项 = %d/%d/%d/%d/%d", rd.name, got[i].ID, got[i].PlaceCount, got[i].VisitCount, got[i].DayCount, got[i].CityCount, got[i].DwellMin, want[i].PlaceCount, want[i].VisitCount, want[i].DayCount, want[i].CityCount, want[i].DwellMin) } if got[i].PlaceCount > 0 { nonZero++ } // 半径 1km 圈在 (1,1) 是海面,必须一个地点都圈不到(证明区域条件真的生效) if got[i].ID == 5 && (got[i].PlaceCount != 0 || got[i].VisitCount != 0 || got[i].DwellMin != 0) { t.Errorf("[%s] 半径 1km 圈在 (1,1) 的专题不应有内容,实得 %d 地点 %d 到访", rd.name, got[i].PlaceCount, got[i].VisitCount) } } if rd.wantNonZero && nonZero == 0 { t.Fatalf("[%s] 所有专题统计都是 0,用例形同虚设", rd.name) } t.Logf("[%s] %d 个专题逐个比对通过(有内容 %d 个)", rd.name, len(got), nonZero) } }