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

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
+60 -5
View File
@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"testing"
@@ -202,23 +203,77 @@ func TestPlaceUpdateBatchActivity(t *testing.T) {
t.Fatalf("打开导出包失败: %v", err)
}
defer edb.Close()
ph := make([]string, len(pks))
ph := make([]string, 0, len(pks))
args := make([]any, 0, len(pks)+1)
for i, v := range pks {
ph[i] = "?"
// ⚠️ 参数顺序必须与 SQL 里占位符的出现顺序一致:`FILTER (WHERE ZACTIVITY_=?)` 在
// SELECT 列表里,它的 `?` 是整个语句的第一个,所以 actSrc 要排在所有 pks 之前。
// 曾经写成先追加 pks、最后追加 actSrc,于是这个 `?` 绑到了 pks[0] 上,
// 同时 IN 列表被顶掉一位、变成 pks[1:] ∪ {actSrc}:条数偶尔少 1(正是历史 flaky 的来源),
// 而 converted 只有碰巧 pks[0] == actSrc 时才等于 246。
args = append(args, actSrc)
for _, v := range pks {
ph = append(ph, "?")
args = append(args, v)
}
args = append(args, actSrc)
var inPackage, converted int
q := `SELECT count(*), count(*) FILTER (WHERE ZACTIVITY_=?) FROM ZVISIT WHERE Z_PK IN (` +
strings.Join(ph, ",") + `)`
if strings.Count(q, "?") != len(args) {
t.Fatalf("占位符 %d 个、参数 %d 个,绑定位错位了", strings.Count(q, "?"), len(args))
}
if err := edb.QueryRow(q, args...).Scan(&inPackage, &converted); err != nil {
t.Fatalf("读导出包失败: %v", err)
}
if inPackage != len(pks) {
t.Fatalf("导出包里只有 %d/%d 条到访", inPackage, len(pks))
// 两种成因的修法完全不同,所以失败时直接把证据打出来:
// ① pks 自身有重复 —— IN 列表里有 246 个值、只命中 245 个不同的行
// ② pks 唯一但包里真缺行 —— 导出/覆盖那条路把某一行弄丢了
missing, dup, total := diffVisitPKs(t, edb, pks)
t.Fatalf("导出包里只有 %d/%d 条到访(包内 ZVISIT 共 %d 行);缺 %v,pks 内重复 %v",
inPackage, len(pks), total, missing, dup)
}
if converted != len(pks) {
t.Fatalf("导出包里只有 %d/%d 条到访的活动换成了 #%d(站点内部 id %d)", converted, len(pks), actSrc, actID)
}
}
// diffVisitPKs 说清「包里的到访为什么比预期少」:返回包里缺的编号、pks 里重复的编号、
// 以及包内 ZVISIT 的总行数。总行数能立刻区分「少写了一行」和「两行撞成一个 Z_PK」。
func diffVisitPKs(t *testing.T, edb *sql.DB, pks []int) (missing, dup []int, total int) {
t.Helper()
have := map[int]bool{}
rows, err := edb.Query(`SELECT Z_PK FROM ZVISIT`)
if err != nil {
t.Fatalf("读导出包的到访编号失败: %v", err)
}
for rows.Next() {
var pk int
if err := rows.Scan(&pk); err != nil {
rows.Close()
t.Fatalf("扫导出包到访编号失败: %v", err)
}
have[pk] = true
total++
}
if err := rows.Err(); err != nil {
rows.Close()
t.Fatalf("读导出包到访编号中断: %v", err)
}
rows.Close()
want := map[int]int{}
for _, v := range pks {
want[v]++
}
for v, c := range want {
if c > 1 {
dup = append(dup, v)
}
if !have[v] {
missing = append(missing, v)
}
}
sort.Ints(missing)
sort.Ints(dup)
return missing, dup, total
}