This commit is contained in:
tamakyi
2026-09-08 10:10:44 +08:00
commit 2ed27dd55e
288 changed files with 67061 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
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)