package logic import ( "context" "sync" "time" modentity "spark-server/internal/module/moderation/entity" "spark-server/internal/pkg/sensitive" ) // MemoryRepo is an in-process word list for logic tests (no MySQL). type MemoryRepo struct { mu sync.Mutex words map[int64]*modentity.Word nextID int64 m *sensitive.Matcher hits []HitRecord } func NewMemoryRepo(words ...*modentity.Word) *MemoryRepo { r := &MemoryRepo{words: map[int64]*modentity.Word{}, nextID: 1} for _, w := range words { if w == nil { continue } if w.ID == 0 { w.ID = r.nextID r.nextID++ } cp := *w r.words[cp.ID] = &cp } r.rebuild() return r } func (r *MemoryRepo) Ready() bool { return r != nil } func (r *MemoryRepo) ReloadMatcher(ctx context.Context) error { _ = ctx r.rebuild() return nil } func (r *MemoryRepo) rebuild() { entries := make([]sensitive.Entry, 0, len(r.words)) for _, w := range r.words { if w == nil || !w.Enabled || w.Word == "" { continue } entries = append(entries, sensitive.Entry{ID: w.ID, Word: w.Word}) } r.m = sensitive.Build(entries) } func (r *MemoryRepo) ListWords(ctx context.Context, _ string, _ int, _ *bool, _ int, _ int) ([]modentity.Word, int64, error) { _ = ctx r.mu.Lock() defer r.mu.Unlock() var all []modentity.Word for _, w := range r.words { if w == nil { continue } all = append(all, *w) } return all, int64(len(all)), nil } func (r *MemoryRepo) CreateWord(ctx context.Context, w *modentity.Word) error { _ = ctx r.mu.Lock() defer r.mu.Unlock() if w.ID == 0 { w.ID = r.nextID r.nextID++ } cp := *w r.words[cp.ID] = &cp r.rebuild() return nil } func (r *MemoryRepo) UpdateWord(ctx context.Context, id int64, patch WordPatch) error { _ = ctx r.mu.Lock() defer r.mu.Unlock() w := r.words[id] if w == nil { return ErrNotReady } cp := *w if patch != nil { if err := patch(&cp); err != nil { return err } } r.words[id] = &cp r.rebuild() return nil } func (r *MemoryRepo) SoftDeleteWord(ctx context.Context, id int64) error { return r.UpdateWord(ctx, id, func(w *modentity.Word) error { w.Enabled = false return nil }) } func (r *MemoryRepo) InsertHits(ctx context.Context, hits []HitRecord) error { _ = ctx r.mu.Lock() defer r.mu.Unlock() r.hits = append(r.hits, hits...) return nil } func (r *MemoryRepo) ListHits(ctx context.Context, _ int64, _ int, _, _ *time.Time, _ int, _ int) ([]HitRecord, int64, error) { _ = ctx r.mu.Lock() defer r.mu.Unlock() return r.hits, int64(len(r.hits)), nil } func (r *MemoryRepo) LookupWord(id int64) *modentity.Word { r.mu.Lock() defer r.mu.Unlock() w := r.words[id] if w == nil { return nil } cp := *w return &cp } func (r *MemoryRepo) MatcherFind(text string) ([]MatchDetail, string, bool) { idx := sensitive.FoldAndIndex(text) if r.m == nil || len(idx.Norm) == 0 { return nil, text, true } raw := r.m.FindAll(idx.Norm) if len(raw) == 0 { return nil, text, true } intervals, ok := sensitive.IntervalsFromMatches(&idx, raw) masked := text if ok { masked = sensitive.Mask(idx.OrigBytes, intervals, "***") } details := make([]MatchDetail, 0, len(raw)) for _, rm := range raw { w := r.LookupWord(rm.EntryID) if w == nil { continue } from, to := 0, 0 if ok && rm.Start >= 0 && rm.End <= len(idx.OrigFrom) { from = idx.OrigFrom[rm.Start] to = idx.OrigTo[rm.End-1] } details = append(details, MatchDetail{ WordID: w.ID, Category: w.Category, Action: w.Action, Excerpt: sensitive.Excerpt(idx.OrigBytes, from, to, 128), From: from, To: to, }) } return details, masked, ok }