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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
170 lines
5.1 KiB
Go
170 lines
5.1 KiB
Go
package stats_test
|
||
|
||
import (
|
||
"context"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
|
||
"rond-with-you/internal/stats"
|
||
)
|
||
|
||
// placeCols 里每个相关子查询都必须带 `<别名>.dataset_id = p.dataset_id`:
|
||
// idx_visits_place 是 (dataset_id, place_id),缺了前导列,每轮循环都要为库里
|
||
// 每个 dataset 各下一次索引(实测 Buffer 命中 23737 → 5669,耗时 53ms → 31ms)。
|
||
// 谓词本身冗余,删掉不会报错、也不会算错,只会悄悄变慢 —— 所以只能这么钉。
|
||
func TestPlaceColsCarriesDatasetIDInCorrelatedSubqueries(t *testing.T) {
|
||
// 去掉全部空白再找,免得因为换行/缩进/等号两边空格的差异误报。
|
||
compact := strings.NewReplacer(" ", "", "\n", "", "\t", "", "\r", "").Replace(stats.PlaceCols)
|
||
// 必须按子查询分段判断:v2 出现三次,全文 Contains 会被另外两处顶包。
|
||
hits := regexp.MustCompile(`FROMvisits(v\d+)`).FindAllStringSubmatchIndex(compact, -1)
|
||
if len(hits) != 5 {
|
||
t.Fatalf("placeCols 里的 visits 子查询数量变成 %d(期望 5),请同步更新本用例", len(hits))
|
||
}
|
||
for i, m := range hits {
|
||
alias := compact[m[2]:m[3]]
|
||
end := len(compact)
|
||
if i+1 < len(hits) {
|
||
end = hits[i+1][0]
|
||
}
|
||
if seg := compact[m[1]:end]; !strings.Contains(seg, alias+".dataset_id=p.dataset_id") {
|
||
t.Errorf("placeCols 中 %s 的子查询缺少 %s.dataset_id=p.dataset_id(会导致索引前导列缺失)", alias, alias)
|
||
}
|
||
}
|
||
}
|
||
|
||
// placeCols 里 top 活动三轴(name/color/icon)是三个**互相独立**的众数,
|
||
// 不能合并成一个 GROUP BY:合并后同一活动名下的颜色/图标会跟着最多的那一行走,
|
||
// 而独立众数允许「名字取 A、颜色取 B」。这里用 Go 侧独立数一遍钉住语义。
|
||
// SQL 的并列取法是未定义的,所以只在「三轴都存在唯一严格最大值」的地点上断言。
|
||
func TestPlaceColsTopActivityMatchesIndependentCount(t *testing.T) {
|
||
db, dsID := setup(t)
|
||
ctx := context.Background()
|
||
d := stats.DB{DB: db}
|
||
|
||
type counts map[string]int
|
||
type placeAgg struct {
|
||
name, color, icon counts
|
||
home, work bool
|
||
}
|
||
aggs := map[int64]*placeAgg{}
|
||
|
||
var placeIDs []int64
|
||
rows, err := db.QueryContext(ctx, `SELECT id FROM places WHERE dataset_id=$1`, dsID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for rows.Next() {
|
||
var id int64
|
||
if err := rows.Scan(&id); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
placeIDs = append(placeIDs, id)
|
||
aggs[id] = &placeAgg{name: counts{}, color: counts{}, icon: counts{}}
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 有活动的到访:top 三轴的独立计数。
|
||
rows, err = db.QueryContext(ctx, `SELECT v.place_id, a.name, a.color, a.icon
|
||
FROM visits v JOIN activities a ON a.id=v.activity_id WHERE v.dataset_id=$1`, dsID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for rows.Next() {
|
||
var pid int64
|
||
var name, color, icon string
|
||
if err := rows.Scan(&pid, &name, &color, &icon); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a := aggs[pid]
|
||
if a == nil {
|
||
continue
|
||
}
|
||
a.name[name]++
|
||
a.color[color]++
|
||
a.icon[icon]++
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// is_home / is_work 来自 visits 本身,不 join activities。
|
||
for _, tc := range []struct {
|
||
q string
|
||
mark func(*placeAgg)
|
||
}{
|
||
{`SELECT place_id FROM visits WHERE dataset_id=$1 AND is_home`, func(a *placeAgg) { a.home = true }},
|
||
{`SELECT place_id FROM visits WHERE dataset_id=$1 AND is_work`, func(a *placeAgg) { a.work = true }},
|
||
} {
|
||
rows, err := db.QueryContext(ctx, tc.q, dsID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for rows.Next() {
|
||
var pid int64
|
||
if err := rows.Scan(&pid); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if a := aggs[pid]; a != nil {
|
||
tc.mark(a)
|
||
}
|
||
}
|
||
rows.Close()
|
||
if err := rows.Err(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
got, err := d.MapPlaces(ctx, stats.Filter{DatasetID: dsID}, 4000)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(got) == 0 {
|
||
t.Skip("MapPlaces 返回空,无法比对")
|
||
}
|
||
|
||
strict := func(c counts) (string, bool) { // 唯一严格众数
|
||
best, n, tie := "", 0, false
|
||
for v, k := range c {
|
||
switch {
|
||
case k > n:
|
||
best, n, tie = v, k, false
|
||
case k == n:
|
||
tie = true
|
||
}
|
||
}
|
||
return best, best != "" && !tie
|
||
}
|
||
|
||
checked := 0
|
||
for _, p := range got {
|
||
a := aggs[p.ID]
|
||
if a == nil {
|
||
t.Fatalf("MapPlaces 返回了不属于本数据集的地点 %d", p.ID)
|
||
}
|
||
if p.IsHome != a.home || p.IsWork != a.work {
|
||
t.Errorf("地点 %d: is_home=%v is_work=%v,独立统计为 %v/%v",
|
||
p.ID, p.IsHome, p.IsWork, a.home, a.work)
|
||
}
|
||
name, nOK := strict(a.name)
|
||
color, cOK := strict(a.color)
|
||
icon, iOK := strict(a.icon)
|
||
if !nOK || !cOK || !iOK {
|
||
continue // SQL 的并列取法未定义,跳过
|
||
}
|
||
checked++
|
||
if p.TopActivity != name || p.TopColor != color || p.TopIcon != icon {
|
||
t.Errorf("地点 %d: top 三轴 = %q/%q/%q,独立数出 %q/%q/%q",
|
||
p.ID, p.TopActivity, p.TopColor, p.TopIcon, name, color, icon)
|
||
}
|
||
}
|
||
if checked == 0 {
|
||
t.Skip("本数据集没有「三轴唯一众数」的地点,比不了")
|
||
}
|
||
t.Logf("已比对 %d/%d 个地点(其余有并列,跳过)", checked, len(got))
|
||
}
|