审计修复:后端(安全 / 数据一致性 / 性能)

audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证
(注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。

安全
- CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板
- 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性)
- 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段
- GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition
- 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品

数据一致性
- 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐)
- Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error
- 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致
- 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows
- 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全

性能
- placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛
- 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍

另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
This commit is contained in:
tamakyi
2026-09-24 20:37:16 +08:00
parent 78865fc736
commit b4c2a8452f
70 changed files with 5417 additions and 360 deletions
+72
View File
@@ -250,3 +250,75 @@ func TestWeatherStatsRespectsFilter(t *testing.T) {
t.Errorf("按城市筛选后行数(%d)不该等于全量(%d)", sum(byCity), wantTotal)
}
}
// TestVisitSideFiltersApplyToMovements 钉住「按活动 / 城市 / 标签 / 关键词筛选时,
// 里程与出行次数也要跟着变」。
//
// movements 自己只有两端到访,没有活动、城市这些字段。曾经 movementsWhere 只认
// dataset / 时间 / 家工作 / 区域,于是筛完之后「到访数变了、里程还是全量」——
// 区域那档犯过一次(见上一个测试),这几档是同一个洞没堵完。
func TestVisitSideFiltersApplyToMovements(t *testing.T) {
db, dsID := setup(t)
d := stats.DB{DB: db}
ctx := context.Background()
// 挑一个真有到访的活动,且它只覆盖一部分到访(否则筛选前后一样,测不出东西)
var actID int64
if err := db.QueryRow(`SELECT activity_id FROM visits
WHERE dataset_id=$1 AND activity_id IS NOT NULL
GROUP BY activity_id ORDER BY count(*) DESC LIMIT 1`, dsID).Scan(&actID); err != nil {
t.Skipf("没有带活动的到访:%v", err)
}
all, err := d.Overview(ctx, stats.Filter{DatasetID: dsID})
if err != nil {
t.Fatal(err)
}
one, err := d.Overview(ctx, stats.Filter{DatasetID: dsID, ActivityIDs: []int64{actID}})
if err != nil {
t.Fatal(err)
}
if one.Visits >= all.Visits {
t.Skipf("活动 %d 覆盖了全部到访,换个数据集再测", actID)
}
// 手写 SQL:两端**任一端**的到访属于该活动的位移
var dist float64
var n int
if err := db.QueryRow(`SELECT COALESCE(sum(m.distance_km),0), count(*) FROM movements m
WHERE m.dataset_id=$1 AND EXISTS (SELECT 1 FROM visits v
WHERE v.dataset_id=m.dataset_id AND v.activity_id=$2
AND (v.src_pk=m.from_visit_src OR v.src_pk=m.to_visit_src))`,
dsID, actID).Scan(&dist, &n); err != nil {
t.Fatal(err)
}
if diff := one.DistanceKm - dist; diff > 0.01 || diff < -0.01 {
t.Errorf("按活动筛选后里程 %.4f,手写 SQL %.4f(筛选没作用到里程上?)", one.DistanceKm, dist)
}
if one.Movements != n {
t.Errorf("按活动筛选后出行次数 %d,手写 SQL %d", one.Movements, n)
}
// 同一条路径也覆盖城市:movementVisitConds 里所有维度共用一个 EXISTS
var city string
if err := db.QueryRow(`SELECT p.city FROM visits v JOIN places p ON p.id=v.place_id
WHERE v.dataset_id=$1 AND p.city IS NOT NULL AND p.city<>''
GROUP BY 1 ORDER BY count(*) DESC LIMIT 1`, dsID).Scan(&city); err != nil {
t.Skipf("没有带城市的到访:%v", err)
}
byCity, err := d.Overview(ctx, stats.Filter{DatasetID: dsID, City: city})
if err != nil {
t.Fatal(err)
}
var cityDist float64
if err := db.QueryRow(`SELECT COALESCE(sum(m.distance_km),0) FROM movements m
WHERE m.dataset_id=$1 AND EXISTS (SELECT 1 FROM visits v
WHERE v.dataset_id=m.dataset_id
AND EXISTS (SELECT 1 FROM places px WHERE px.id=v.place_id AND px.city=$2)
AND (v.src_pk=m.from_visit_src OR v.src_pk=m.to_visit_src))`,
dsID, city).Scan(&cityDist); err != nil {
t.Fatal(err)
}
if diff := byCity.DistanceKm - cityDist; diff > 0.01 || diff < -0.01 {
t.Errorf("按城市「%s」筛选后里程 %.4f,手写 SQL %.4f", city, byCity.DistanceKm, cityDist)
}
}