package model import ( "context" "errors" "time" mysqldriver "github.com/go-sql-driver/mysql" "gorm.io/gorm" modentity "spark-server/internal/module/moderation/entity" "spark-server/internal/pkg/errs" ) type Repo struct { db *gorm.DB matcher *MatcherStore } func NewRepo(db *gorm.DB) *Repo { if db == nil { return nil } return &Repo{db: db, matcher: NewMatcherStore()} } func (r *Repo) Ready() bool { return r != nil && r.db != nil } func (r *Repo) Matcher() *MatcherStore { if r == nil { return nil } return r.matcher } func (r *Repo) ReloadMatcher(ctx context.Context) error { if !r.Ready() { return errs.ErrAvailable } var rows []WordRow if err := r.db.WithContext(ctx).Where("enabled = ?", 1).Find(&rows).Error; err != nil { return err } r.matcher.Replace(rows) return nil } func (r *Repo) ListWords(ctx context.Context, q string, category int, enabled *bool, page, size int) ([]modentity.Word, int64, error) { if !r.Ready() { return nil, 0, errs.ErrAvailable } if size <= 0 || size > 200 { size = 50 } if page < 0 { page = 0 } db := r.db.WithContext(ctx).Model(&WordRow{}) q = trim(q) if q != "" { like := "%" + q + "%" db = db.Where("word_raw LIKE ? OR word LIKE ?", like, like) } if category > 0 { db = db.Where("category = ?", category) } if enabled != nil { if *enabled { db = db.Where("enabled = ?", 1) } else { db = db.Where("enabled = ?", 0) } } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var rows []WordRow if err := db.Order("id DESC").Offset(page * size).Limit(size).Find(&rows).Error; err != nil { return nil, 0, err } out := make([]modentity.Word, 0, len(rows)) for i := range rows { out = append(out, *rows[i].entity()) } return out, total, nil } func (r *Repo) CreateWord(ctx context.Context, w *modentity.Word) error { if !r.Ready() || w == nil || w.Word == "" { return errs.ErrInvalidArgument } now := time.Now() row := WordRow{ WordRaw: w.WordRaw, Word: w.Word, Category: int8(w.Category), Action: int8(w.Action), MuteSeconds: w.MuteSeconds, Enabled: boolToTiny(w.Enabled), CreatedAt: now, UpdatedAt: now, } if err := r.db.WithContext(ctx).Create(&row).Error; err != nil { return mapDB(err) } w.ID = row.ID w.CreatedAt = now w.UpdatedAt = now return r.ReloadMatcher(ctx) } func (r *Repo) UpdateWord(ctx context.Context, id int64, patch func(*WordRow) error) error { if !r.Ready() || id <= 0 { return errs.ErrInvalidArgument } var row WordRow if err := r.db.WithContext(ctx).Where("id = ?", id).Take(&row).Error; err != nil { return mapDB(err) } if patch != nil { if err := patch(&row); err != nil { return err } } row.UpdatedAt = time.Now() if err := r.db.WithContext(ctx).Save(&row).Error; err != nil { return mapDB(err) } return r.ReloadMatcher(ctx) } func (r *Repo) SoftDeleteWord(ctx context.Context, id int64) error { return r.UpdateWord(ctx, id, func(row *WordRow) error { row.Enabled = 0 return nil }) } func (r *Repo) InsertHits(ctx context.Context, rows []HitRow) error { if !r.Ready() || len(rows) == 0 { return nil } return r.db.WithContext(ctx).Create(&rows).Error } func (r *Repo) ListHits(ctx context.Context, userID int64, category int, since, until *time.Time, page, size int) ([]HitRow, int64, error) { if !r.Ready() { return nil, 0, errs.ErrAvailable } if size <= 0 || size > 200 { size = 50 } if page < 0 { page = 0 } db := r.db.WithContext(ctx).Model(&HitRow{}) if userID > 0 { db = db.Where("user_id = ?", userID) } if category > 0 { db = db.Where("category = ?", category) } if since != nil { db = db.Where("created_at >= ?", *since) } if until != nil { db = db.Where("created_at <= ?", *until) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var rows []HitRow if err := db.Order("created_at DESC").Offset(page * size).Limit(size).Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil } func boolToTiny(v bool) int8 { if v { return 1 } return 0 } func trim(s string) string { for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') { s = s[1:] } for len(s) > 0 { last := s[len(s)-1] if last != ' ' && last != '\t' { break } s = s[:len(s)-1] } return s } func mapDB(err error) error { if err == nil { return nil } if errors.Is(err, gorm.ErrRecordNotFound) { return errs.ErrNotFound } var me *mysqldriver.MySQLError if errors.As(err, &me) && me.Number == 1062 { return errs.ErrDuplicate } return err }