Files
jiang13-forum/services/ratelimit.go
freefire 3f50316ad0 fix: 完成 Gitea 目录改组收尾(import、构建与 LICENSE)
同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 04:01:20 +08:00

89 lines
1.8 KiB
Go

package services
import (
"sync"
"time"
)
// RateLimiter 简单内存限流器,防止重复刷屏
type RateLimiter struct {
mu sync.Mutex
records map[string][]time.Time
settings *ForumSettingsService
}
func NewRateLimiter(settings *ForumSettingsService) *RateLimiter {
r := &RateLimiter{
records: make(map[string][]time.Time),
settings: settings,
}
go r.cleanup()
return r
}
// Allow 检查 action+key 是否允许操作
func (r *RateLimiter) Allow(action, key string) bool {
limit := r.limitFor(action)
window := r.windowFor(action)
if limit <= 0 {
return true
}
fullKey := action + ":" + key
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
cutoff := now.Add(-window)
times := r.records[fullKey]
var valid []time.Time
for _, t := range times {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) >= limit {
r.records[fullKey] = valid
return false
}
valid = append(valid, now)
r.records[fullKey] = valid
return true
}
func (r *RateLimiter) limitFor(action string) int {
if action == "friend_link" {
return 5
}
return r.settings.RateLimitFor(action)
}
func (r *RateLimiter) windowFor(action string) time.Duration {
if action == "friend_link" {
return time.Hour
}
return time.Duration(r.settings.RateLimitWindowSec()) * time.Second
}
func (r *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
r.mu.Lock()
// 使用最大窗口清理,覆盖友链 1 小时窗口
cutoff := time.Now().Add(-time.Hour * 2)
for k, times := range r.records {
var valid []time.Time
for _, t := range times {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) == 0 {
delete(r.records, k)
} else {
r.records[k] = valid
}
}
r.mu.Unlock()
}
}