package shared import ( "crypto/md5" "encoding/hex" "strings" "sync" "time" ) // DuplicateBlocker 阻止同一来源(IP + 内容指纹)在窗口期内的重复提交, // 用于拦截刷屏 / 重复轰炸提问。 // // 实现:内存 map,key = normalizedIP + "|" + contentHash,值为首次提交时间。 // 窗口期内再次提交相同内容返回 false(拒绝)。 // // 内存安全:条数超过 50000 时先清理过期项;窗口外的 key 由后台清理循环定时移除。 type DuplicateBlocker struct { mu sync.Mutex items map[string]time.Time window time.Duration maxItem int } // NewDuplicateBlocker 创建拦截器并启动后台清理循环。 // 注意:每个实例会启动一个常驻 goroutine,应用内应使用包级单例 DuplicateGuard, // 不要为每次请求创建新实例。 func NewDuplicateBlocker(window time.Duration) *DuplicateBlocker { d := &DuplicateBlocker{ items: make(map[string]time.Time), window: window, maxItem: 50000, } go func() { for { time.Sleep(10 * time.Minute) d.cleanup() } }() return d } // Allow 判断 key(IP+内容)是否允许提交: // - 窗口期内已存在相同 key → 返回 false(重复,拒绝) // - 否则记录并返回 true func (d *DuplicateBlocker) Allow(key string) bool { now := time.Now() cutoff := now.Add(-d.window) d.mu.Lock() defer d.mu.Unlock() if t, ok := d.items[key]; ok && t.After(cutoff) { return false } // 超限时先清理过期项,避免恶意构造大量 key 撑爆内存 if len(d.items) >= d.maxItem { for k, t := range d.items { if !t.After(cutoff) { delete(d.items, k) } } // 仍超限则整体放弃写入(宁可放行也不撑爆内存) if len(d.items) >= d.maxItem { return true } } d.items[key] = now return true } func (d *DuplicateBlocker) cleanup() { cutoff := time.Now().Add(-d.window) d.mu.Lock() for k, t := range d.items { if !t.After(cutoff) { delete(d.items, k) } } d.mu.Unlock() } // DuplicateKey 生成拦截 key:规范化 IP + 内容 MD5。 func DuplicateKey(ip, content string) string { ip = NormalizeIP(ip) sum := md5.Sum([]byte(strings.TrimSpace(content))) return ip + "|" + hex.EncodeToString(sum[:]) } // DuplicateGuard 全局限流实例:同 IP 在 5 分钟内提交相同内容会被拒绝。 // 阈值合理:正常用户几乎不可能在 5 分钟内提交两条完全一致的提问。 var DuplicateGuard = NewDuplicateBlocker(5 * time.Minute)