修地图城市圈的聚合口径:先按地点汇总,再按城市汇总

/api/cities 原来把聚合打在 places ⋈ visits 上:一个地点有 N 条到访,它的 visit_count 与
dwell_minutes 就被累加 N 次,count(*) 数出来的到访条数又被当成「地点数」显示——实测最大的
那个城市气泡写着「到访 72661 次 · 568 个地点」,真值是 568 次 / 86 个地点(差 128 倍)。
改成内层按地点 GROUP BY、外层按城市 sum/count,并补上「已并入 / 子段不算数」的判据,
圈的位置、排序与返回列序都不变。

回归 city_agg_test.go:手写同口径 SQL 对账 + 「地点数不可能多于到访数」的不变量;
变异验证(sum(pa.visits) *2)立刻红。
This commit is contained in:
tamakyi
2026-09-29 22:07:23 +08:00
parent 4597047989
commit 3d03aae018
2 changed files with 108 additions and 10 deletions
+20 -10
View File
@@ -948,16 +948,26 @@ func (s *Server) apiCityAgg(w http.ResponseWriter, r *http.Request) {
if !p.IsAdmin() {
regSQL = regionSQL("p", p.Settings.Regions, p.Settings.RegionBlacklist(), &args)
}
rows, err := s.db.QueryContext(r.Context(), `SELECT COALESCE(p.city,''), COALESCE(p.province,''),
AVG(p.lat)::float8, AVG(p.lon)::float8,
COALESCE(sum(p.visit_count),0), count(*), COALESCE(sum(p.dwell_minutes),0),
min(p.first_visit_at), max(p.last_visit_at)
FROM places p
JOIN visits v ON v.place_id = p.id AND v.dataset_id = p.dataset_id
WHERE p.dataset_id = $1 AND p.city IS NOT NULL AND p.city <> '' AND p.lat IS NOT NULL`+regSQL+`
AND NOT EXISTS (SELECT 1 FROM visits vh WHERE vh.place_id = p.id
AND (vh.is_home OR vh.is_work))
GROUP BY p.city, p.province
// 先按**地点**聚合、再按城市汇总。直接在 places ⋈ visits 上 sum(places.visit_count) 是错的:
// 一个地点有 N 条到访,它的 visit_count 就会被累加 N 次(实测「到访 72661 次」,真值 568),
// dwell 同理;count(*) 数出来的是到访条数,却被当成「地点数」显示。
rows, err := s.db.QueryContext(r.Context(), `SELECT COALESCE(pa.city,''), COALESCE(pa.province,''),
AVG(pa.lat)::float8, AVG(pa.lon)::float8,
COALESCE(sum(pa.visits),0), count(*), COALESCE(sum(pa.dwell),0),
min(pa.first_at), max(pa.last_at)
FROM (
SELECT p.id, p.city, p.province, p.lat, p.lon, p.first_visit_at AS first_at, p.last_visit_at AS last_at,
count(*) AS visits,
COALESCE(sum(LEAST(COALESCE(v.duration_min,0),10080)),0) AS dwell
FROM places p
JOIN visits v ON v.place_id = p.id AND v.dataset_id = p.dataset_id
WHERE p.dataset_id = $1 AND p.city IS NOT NULL AND p.city <> '' AND p.lat IS NOT NULL`+regSQL+`
AND `+stats.SoloVisitSQL("v")+`
AND NOT EXISTS (SELECT 1 FROM visits vh WHERE vh.place_id = p.id
AND (vh.is_home OR vh.is_work))
GROUP BY p.id
) pa
GROUP BY pa.city, pa.province
ORDER BY 5 DESC`, args...)
if err != nil {
s.json(w, map[string]string{"error": err.Error()})