Files
tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
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 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00

84 lines
2.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"context"
"fmt"
"log"
"runtime/debug"
"sync"
"time"
)
// parallelLimit 是页面内并行查询的并发上限。
// 站点常跑在 1C1G 的小机器上,并发太高会把 PostgreSQL 和 CPU 一起挤住,
// 反而比串行更慢(同一份磁盘在排队)。
const parallelLimit = 6
// slowJobLog 是打慢查询日志的阈值(debug 模式下)。
const slowJobLog = 30 * time.Millisecond
// pageJob 是一次页面内的独立查询。name 只用于日志,方便定位是哪个查询慢。
type pageJob struct {
name string
run func(context.Context) error
}
// runJob 跑一个任务,并把 panic 收敛成错误。
//
// 这层兜底不能省:任务跑在子 goroutine 里,而 HTTP 中间件的 recover 只覆盖请求
// goroutine(server.go 的 s.recover),子 goroutine 里逃出去的 panic 会直接终止
// **整个进程**——表现是「站点进程消失」,而不是这一页返回 500。
func (s *Server) runJob(ctx context.Context, j pageJob) (err error) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("页面查询 %s 崩溃: %v\n%s", j.name, rec, debug.Stack())
err = fmt.Errorf("查询 %s 内部错误", j.name)
}
}()
return j.run(ctx)
}
// runJobs 并发执行互不依赖的查询,返回第一个(按传入顺序)非 nil 错误。
//
// 统计页这类页面有十几个互不依赖的查询,串行执行时页面耗时≈它们之和(实测 ~400ms),
// 并发之后≈最慢的那一个。**任务之间不能写同一份数据**:这里只保证并发安全,
// 不管业务上的写入冲突。
func (s *Server) runJobs(ctx context.Context, jobs ...pageJob) error {
if len(jobs) <= 1 {
for _, j := range jobs {
if err := s.runJob(ctx, j); err != nil {
return err
}
}
return nil
}
sem := make(chan struct{}, parallelLimit)
errs := make([]error, len(jobs))
var wg sync.WaitGroup
for i := range jobs {
wg.Add(1)
go func(i int) {
defer wg.Done()
select {
case sem <- struct{}{}:
case <-ctx.Done():
errs[i] = ctx.Err()
return
}
defer func() { <-sem }()
start := time.Now()
errs[i] = s.runJob(ctx, jobs[i])
if d := time.Since(start); d > slowJobLog && s.cfg.Debug {
log.Printf("慢查询 %s: %v", jobs[i].name, d.Round(time.Millisecond))
}
}(i)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}