审计修复:后端(安全 / 数据一致性 / 性能)
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user