Files
agentic-coding-workflow/internal/infra/notify/prefs.go
T
2026-06-22 08:55:57 +08:00

108 lines
3.6 KiB
Go

// Package notify 内的 prefs.go 实现 per-user 通知偏好(notification_prefs 表)。
//
// 偏好控制 email / im 两条外发通道的开关与最低投递级别(min_severity)。in-app
// inbox(持久化 + SSE)始终投递,不受偏好影响。im_webhook_url 以密文落库
// (im_webhook_url_enc BYTEA,经 crypto.Encryptor 加密),repo 层只搬运密文,
// 解密由 IMNotifier 在发送时按需进行——避免明文 webhook URL 进入领域内存层。
package notify
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
notifysqlc "github.com/yan1h/agent-coding-workflow/internal/infra/notify/sqlc"
)
// Prefs 是一个用户的通知偏好领域形态。ImWebhookURLEnc 保持密文(BYTEA 原样),
// 解密延迟到 IMNotifier.Send。MinSeverity 取值与 Severity 枚举一致。
type Prefs struct {
UserID uuid.UUID
EmailEnabled bool
IMEnabled bool
ImWebhookURLEnc []byte
MinSeverity Severity
}
// DefaultPrefs 是用户尚未显式设置偏好时的兜底:email / im 均关闭,最低级别
// warning(与 notification_prefs.min_severity 列默认值一致)。
func DefaultPrefs(userID uuid.UUID) Prefs {
return Prefs{
UserID: userID,
EmailEnabled: false,
IMEnabled: false,
MinSeverity: SeverityWarning,
}
}
// PrefsRepository 抽象 notification_prefs 的读写。Get 在无记录时返回
// DefaultPrefs(不报 not-found),让上层无需区分 "未设置" 与 "全关"。
type PrefsRepository interface {
Get(ctx context.Context, userID uuid.UUID) (Prefs, error)
Upsert(ctx context.Context, p Prefs) error
}
// pgPrefsRepo 是 PrefsRepository 的 Postgres 实现。
type pgPrefsRepo struct {
q *notifysqlc.Queries
}
// NewPostgresPrefsRepository 用现有 pgxpool 构造偏好仓库。
func NewPostgresPrefsRepository(pool *pgxpool.Pool) PrefsRepository {
return &pgPrefsRepo{q: notifysqlc.New(pool)}
}
// Get 读取用户偏好。无记录时返回 DefaultPrefs(userID) 与 nil error。
func (r *pgPrefsRepo) Get(ctx context.Context, userID uuid.UUID) (Prefs, error) {
row, err := r.q.GetNotificationPrefs(ctx, toPgUUID(userID))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return DefaultPrefs(userID), nil
}
return Prefs{}, errs.Wrap(err, errs.CodeInternal, "get notification prefs")
}
return Prefs{
UserID: fromPgUUID(row.UserID),
EmailEnabled: row.EmailEnabled,
IMEnabled: row.ImEnabled,
ImWebhookURLEnc: row.ImWebhookUrlEnc,
MinSeverity: Severity(row.MinSeverity),
}, nil
}
// Upsert 写入(或更新)用户偏好。ImWebhookURLEnc 为 nil 时写 NULL。
func (r *pgPrefsRepo) Upsert(ctx context.Context, p Prefs) error {
if err := r.q.UpsertNotificationPrefs(ctx, notifysqlc.UpsertNotificationPrefsParams{
UserID: toPgUUID(p.UserID),
EmailEnabled: p.EmailEnabled,
ImEnabled: p.IMEnabled,
ImWebhookUrlEnc: p.ImWebhookURLEnc,
MinSeverity: string(p.MinSeverity),
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "upsert notification prefs")
}
return nil
}
// severityRank 把 Severity 映射为可比较的等级序数,用于 min_severity 过滤。
// 未知值按最低(info)处理,确保 "至少投递" 而非误丢。
func severityRank(s Severity) int {
switch s {
case SeverityError:
return 2
case SeverityWarning:
return 1
default:
return 0
}
}
// meetsMinSeverity 报告消息级别是否达到(>=)用户设定的最低投递级别。
func meetsMinSeverity(msg Severity, min Severity) bool {
return severityRank(msg) >= severityRank(min)
}