审计修复:后端(安全 / 数据一致性 / 性能)
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:
@@ -0,0 +1,107 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 列表页(fillTopicStatsBatch:一条 UNION ALL)与详情页(topicContent:逐专题查询)
|
||||
// 对同一专题必须给出同样的统计 —— 列表上的数字点进去要对得上。
|
||||
// 顺带钉住批量版的占位符编号:错一个就是 pgx「expected N arguments, got M」。
|
||||
func TestTopicStatsBatchMatchesDetailPage(t *testing.T) {
|
||||
f := newLoginFixture(t)
|
||||
ctx := context.Background()
|
||||
var ds int64
|
||||
if err := f.srv.db.QueryRowContext(ctx, `SELECT id FROM datasets WHERE is_active LIMIT 1`).Scan(&ds); err != nil {
|
||||
t.Skipf("没有生效数据集:%v", err)
|
||||
}
|
||||
var minAt, maxAt sql.NullTime
|
||||
if err := f.srv.db.QueryRowContext(ctx,
|
||||
`SELECT min(arrival), max(arrival) FROM visits WHERE dataset_id=$1`, ds).Scan(&minAt, &maxAt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !minAt.Valid || !maxAt.Valid {
|
||||
t.Skip("生效数据集里没有到访记录")
|
||||
}
|
||||
|
||||
var lat, lon float64
|
||||
if err := f.srv.db.QueryRowContext(ctx, `SELECT COALESCE(lat,0), COALESCE(lon,0) FROM places
|
||||
WHERE dataset_id=$1 ORDER BY visit_count DESC LIMIT 1`, ds).Scan(&lat, &lon); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var city string
|
||||
_ = f.srv.db.QueryRowContext(ctx, `SELECT city FROM places WHERE dataset_id=$1
|
||||
AND COALESCE(city,'')<>'' GROUP BY city ORDER BY count(*) DESC LIMIT 1`, ds).Scan(&city)
|
||||
|
||||
mid := minAt.Time.Add(maxAt.Time.Sub(minAt.Time) / 2)
|
||||
base := []Topic{
|
||||
{ID: 1, Title: "全时段", StartAt: minAt.Time, EndAt: maxAt.Time},
|
||||
{ID: 2, Title: "前半段", StartAt: minAt.Time, EndAt: mid},
|
||||
{ID: 3, Title: "后半段", StartAt: mid, EndAt: maxAt.Time},
|
||||
{ID: 4, Title: "按区域圈定", StartAt: minAt.Time, EndAt: maxAt.Time,
|
||||
CenterLat: lat, CenterLon: lon, RadiusKm: 30},
|
||||
{ID: 5, Title: "区域圈不到任何地点", StartAt: minAt.Time, EndAt: maxAt.Time,
|
||||
CenterLat: 1, CenterLon: 1, RadiusKm: 1},
|
||||
}
|
||||
|
||||
type round struct {
|
||||
name string
|
||||
regions []string
|
||||
exclude bool
|
||||
wantNonZero bool
|
||||
}
|
||||
rounds := []round{{name: "无区域过滤", wantNonZero: true}}
|
||||
if city != "" {
|
||||
rounds = append(rounds,
|
||||
round{name: "白名单:" + city, regions: []string{city}, wantNonZero: true},
|
||||
round{name: "黑名单:" + city, regions: []string{city}, exclude: true},
|
||||
)
|
||||
}
|
||||
|
||||
for _, rd := range rounds {
|
||||
// 详情页路径:逐专题查询
|
||||
want := append([]Topic(nil), base...)
|
||||
for i := range want {
|
||||
points, visits, days, cities, err := f.srv.topicContent(ctx, &want[i], ds, rd.regions, rd.exclude)
|
||||
if err != nil {
|
||||
t.Fatalf("[%s] topicContent #%d: %v", rd.name, want[i].ID, err)
|
||||
}
|
||||
want[i].PlaceCount, want[i].VisitCount = len(points), visits
|
||||
want[i].DayCount, want[i].CityCount = days, cities
|
||||
for _, p := range points {
|
||||
want[i].DwellMin += p.Dwell
|
||||
}
|
||||
}
|
||||
|
||||
// 列表页路径:一条 UNION ALL
|
||||
got := append([]Topic(nil), base...)
|
||||
if err := f.srv.fillTopicStatsBatch(ctx, got, ds, rd.regions, rd.exclude); err != nil {
|
||||
t.Fatalf("[%s] fillTopicStatsBatch: %v", rd.name, err)
|
||||
}
|
||||
|
||||
nonZero := 0
|
||||
for i := range got {
|
||||
if got[i].PlaceCount != want[i].PlaceCount || got[i].VisitCount != want[i].VisitCount ||
|
||||
got[i].DayCount != want[i].DayCount || got[i].CityCount != want[i].CityCount ||
|
||||
got[i].DwellMin != want[i].DwellMin {
|
||||
t.Errorf("[%s] 专题 #%d 批量与详情页不一致:\n 批量 地点/到访/天数/城市/停留 = %d/%d/%d/%d/%d\n 详情页同项 = %d/%d/%d/%d/%d",
|
||||
rd.name, got[i].ID, got[i].PlaceCount, got[i].VisitCount, got[i].DayCount,
|
||||
got[i].CityCount, got[i].DwellMin,
|
||||
want[i].PlaceCount, want[i].VisitCount, want[i].DayCount, want[i].CityCount, want[i].DwellMin)
|
||||
}
|
||||
if got[i].PlaceCount > 0 {
|
||||
nonZero++
|
||||
}
|
||||
// 半径 1km 圈在 (1,1) 是海面,必须一个地点都圈不到(证明区域条件真的生效)
|
||||
if got[i].ID == 5 && (got[i].PlaceCount != 0 || got[i].VisitCount != 0 || got[i].DwellMin != 0) {
|
||||
t.Errorf("[%s] 半径 1km 圈在 (1,1) 的专题不应有内容,实得 %d 地点 %d 到访",
|
||||
rd.name, got[i].PlaceCount, got[i].VisitCount)
|
||||
}
|
||||
}
|
||||
if rd.wantNonZero && nonZero == 0 {
|
||||
t.Fatalf("[%s] 所有专题统计都是 0,用例形同虚设", rd.name)
|
||||
}
|
||||
t.Logf("[%s] %d 个专题逐个比对通过(有内容 %d 个)", rd.name, len(got), nonZero)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user