package store import ( "context" "strings" ) // AppendAudit 追加一条审计留痕(best-effort:审计失败不应影响主流程,调用方忽略返回)。 func (p *Postgres) AppendAudit(ctx context.Context, a *AuditLog) error { if p.db == nil || a == nil { return errStoreDisabled } return p.db.WithContext(ctx).Create(a).Error } // AuditFilter 是审计流的服务端筛选条件。筛选必须落到 SQL:审计的用途是查证, // 若只在“当前页”里筛,搜不到就等于给出“没有这条记录”的错误结论。 type AuditFilter struct { Action string // HTTP 方法,精确匹配 Path string // 路径前缀 Q string // 跨 actor / ip / detail / path 的模糊匹配 } // escapeLike 转义 LIKE 的通配符,让用户输入的 % 和 _ 按字面量匹配 // (否则搜 "100%" 会退化成匹配任意串)。 // 配套的 SQL 必须显式写 ESCAPE '\\':Postgres 默认就拿反斜杠当转义符,但 SQLite 不写 // ESCAPE 就压根没有转义符——依赖隐式默认会在换库/单测时静默失效。 func escapeLike(s string) string { return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s) } // ListAudit 倒序列出审计留痕(管理端审计流;limit 限流、offset 翻页,筛选走 SQL)。 func (p *Postgres) ListAudit(ctx context.Context, limit, offset int, f AuditFilter) ([]AuditLog, error) { if p.db == nil { return nil, errStoreDisabled } limit, offset = clampPage(limit, offset) q := p.db.WithContext(ctx).Model(&AuditLog{}) if f.Action != "" { q = q.Where("action = ?", f.Action) } if f.Path != "" { q = q.Where(`path LIKE ? ESCAPE '\'`, escapeLike(f.Path)+"%") } if f.Q != "" { like := "%" + strings.ToLower(escapeLike(f.Q)) + "%" // 用 LOWER()+LIKE 而非 Postgres 专有的 ILIKE:语义一样,但 SQLite 也支持, // 于是这段能被内存库单测覆盖(审计搜索量小,放弃索引可忽略)。 // 括号不能省:gorm 把每个 Where 以 AND 拼接,裸的 OR 串会让优先级变成 // `action = ? AND actor LIKE ? OR ip LIKE ? ...`,前面的条件直接失效。 q = q.Where(`(LOWER(actor) LIKE ? ESCAPE '\' OR LOWER(ip) LIKE ? ESCAPE '\' `+ `OR LOWER(detail) LIKE ? ESCAPE '\' OR LOWER(path) LIKE ? ESCAPE '\')`, like, like, like, like) } var out []AuditLog err := q.Order("created_at desc").Limit(limit).Offset(offset).Find(&out).Error return out, err } // AppendGuardrailEvent 追加一条护栏命中(best-effort)。 func (p *Postgres) AppendGuardrailEvent(ctx context.Context, e *GuardrailEvent) error { if p.db == nil || e == nil { return errStoreDisabled } return p.db.WithContext(ctx).Create(e).Error } // ListGuardrailEvents 倒序列出护栏事件(管理端安全事件流)。 func (p *Postgres) ListGuardrailEvents(ctx context.Context, limit, offset int) ([]GuardrailEvent, error) { if p.db == nil { return nil, errStoreDisabled } limit, offset = clampPage(limit, offset) var out []GuardrailEvent err := p.db.WithContext(ctx).Order("created_at desc").Limit(limit).Offset(offset).Find(&out).Error return out, err } // clampPage 归一分页参数:limit ∈ [1,200](默认 50),offset ≥ 0。 func clampPage(limit, offset int) (int, int) { if limit <= 0 || limit > 200 { limit = 50 } if offset < 0 { offset = 0 } return limit, offset }