spark-server/internal/module/moderation/logic/memory_repo.go
2026-09-29 16:37:36 +08:00

169 lines
3.5 KiB
Go

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
}